From c6c10032924aaf4eb1646a4fd593c17a7e2ecb3b Mon Sep 17 00:00:00 2001 From: wangxinxin08 <69842442+wangxinxin08@users.noreply.github.com> Date: Thu, 3 Nov 2022 10:53:38 +0800 Subject: [PATCH] add ppyoloe_r (#7105) * add ppyoloe_r * modify code of ops.py * add ppyoloe_r docs and modify rotate docs * modify docs and refine connfigs * fix some problems * refine docs, add nms_rotated ext_op and fix some problems * add image and inference_benchmark.py * modify docs * fix some problems * modify code accroding to review Co-authored-by: wangxinxin08 <> --- configs/datasets/dota_ms.yml | 21 + configs/rotate/README.md | 10 +- configs/rotate/README_en.md | 10 +- configs/rotate/fcosr/README.md | 2 +- configs/rotate/fcosr/README_en.md | 2 +- configs/rotate/ppyoloe_r/README.md | 147 ++++++ configs/rotate/ppyoloe_r/README_en.md | 145 ++++++ .../rotate/ppyoloe_r/_base_/optimizer_3x.yml | 19 + .../rotate/ppyoloe_r/_base_/ppyoloe_r_crn.yml | 49 ++ .../ppyoloe_r/_base_/ppyoloe_r_reader.yml | 45 ++ .../ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml | 15 + .../ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml | 15 + .../ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml | 15 + .../ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml | 15 + .../ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml | 15 + .../ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml | 15 + .../ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml | 15 + .../ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml | 15 + configs/rotate/tools/inference_benchmark.py | 356 +++++++++++++++ docs/MODEL_ZOO_cn.md | 4 +- docs/MODEL_ZOO_en.md | 7 +- docs/images/ppyoloe_r_map_fps.png | Bin 0 -> 148038 bytes .../matched_rbox_iou.cc} | 35 +- .../matched_rbox_iou.cu} | 23 +- ppdet/ext_op/csrc/nms_rotated/nms_rotated.cc | 121 +++++ ppdet/ext_op/csrc/nms_rotated/nms_rotated.cu | 96 ++++ ppdet/ext_op/csrc/rbox_iou/rbox_iou.cc | 95 ++++ .../rbox_iou/{rbox_iou_op.cu => rbox_iou.cu} | 17 +- ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.cc | 97 ---- .../{rbox_iou_op.h => rbox_iou_utils.h} | 16 +- ppdet/modeling/assigners/__init__.py | 2 + ppdet/modeling/assigners/fcosr_assigner.py | 2 +- .../rotated_task_aligned_assigner.py | 164 +++++++ ppdet/modeling/heads/__init__.py | 2 + ppdet/modeling/heads/fcosr_head.py | 4 +- ppdet/modeling/heads/ppyoloe_r_head.py | 419 ++++++++++++++++++ ppdet/modeling/initializer.py | 6 + ppdet/modeling/necks/custom_pan.py | 22 +- ppdet/modeling/ops.py | 16 +- ppdet/modeling/rbox_utils.py | 54 +++ 40 files changed, 1954 insertions(+), 174 deletions(-) create mode 100644 configs/datasets/dota_ms.yml create mode 100644 configs/rotate/ppyoloe_r/README.md create mode 100644 configs/rotate/ppyoloe_r/README_en.md create mode 100644 configs/rotate/ppyoloe_r/_base_/optimizer_3x.yml create mode 100644 configs/rotate/ppyoloe_r/_base_/ppyoloe_r_crn.yml create mode 100644 configs/rotate/ppyoloe_r/_base_/ppyoloe_r_reader.yml create mode 100644 configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml create mode 100644 configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml create mode 100644 configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml create mode 100644 configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml create mode 100644 configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml create mode 100644 configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml create mode 100644 configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml create mode 100644 configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml create mode 100644 configs/rotate/tools/inference_benchmark.py create mode 100644 docs/images/ppyoloe_r_map_fps.png rename ppdet/ext_op/csrc/{rbox_iou/matched_rbox_iou_op.cc => matched_rbox_iou/matched_rbox_iou.cc} (68%) rename ppdet/ext_op/csrc/{rbox_iou/matched_rbox_iou_op.cu => matched_rbox_iou/matched_rbox_iou.cu} (72%) create mode 100644 ppdet/ext_op/csrc/nms_rotated/nms_rotated.cc create mode 100644 ppdet/ext_op/csrc/nms_rotated/nms_rotated.cu create mode 100644 ppdet/ext_op/csrc/rbox_iou/rbox_iou.cc rename ppdet/ext_op/csrc/rbox_iou/{rbox_iou_op.cu => rbox_iou.cu} (90%) delete mode 100644 ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.cc rename ppdet/ext_op/csrc/rbox_iou/{rbox_iou_op.h => rbox_iou_utils.h} (97%) create mode 100644 ppdet/modeling/assigners/rotated_task_aligned_assigner.py create mode 100644 ppdet/modeling/heads/ppyoloe_r_head.py diff --git a/configs/datasets/dota_ms.yml b/configs/datasets/dota_ms.yml new file mode 100644 index 000000000..802e8846d --- /dev/null +++ b/configs/datasets/dota_ms.yml @@ -0,0 +1,21 @@ +metric: RBOX +num_classes: 15 + +TrainDataset: + !COCODataSet + image_dir: trainval1024/images + anno_path: trainval1024/DOTA_trainval1024.json + dataset_dir: dataset/dota_ms/ + data_fields: ['image', 'gt_bbox', 'gt_class', 'is_crowd', 'gt_poly'] + +EvalDataset: + !COCODataSet + image_dir: trainval1024/images + anno_path: trainval1024/DOTA_trainval1024.json + dataset_dir: dataset/dota_ms/ + data_fields: ['image', 'gt_bbox', 'gt_class', 'is_crowd', 'gt_poly'] + +TestDataset: + !ImageFolder + anno_path: test1024/DOTA_test1024.json + dataset_dir: dataset/dota_ms/ diff --git a/configs/rotate/README.md b/configs/rotate/README.md index 574cb4ed5..5558c4a87 100644 --- a/configs/rotate/README.md +++ b/configs/rotate/README.md @@ -16,7 +16,15 @@ | 模型 | mAP | 学习率策略 | 角度表示 | 数据增广 | GPU数目 | 每GPU图片数目 | 模型下载 | 配置文件 | |:---:|:----:|:---------:|:-----:|:--------:|:-----:|:------------:|:-------:|:------:| | [S2ANet](./s2anet/README.md) | 73.84 | 2x | le135 | - | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/s2anet_alignconv_2x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/s2anet/s2anet_alignconv_2x_dota.yml) | -| [FCOSR](./fcosr/README.md) | 76.62 | 3x | oc | - | 4 | 4 | [model](https://paddledet.bj.bcebos.com/models/fcosr_x50_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/fcosr/fcosr_x50_3x_dota.yml) | +| [FCOSR](./fcosr/README.md) | 76.62 | 3x | oc | RR | 4 | 4 | [model](https://paddledet.bj.bcebos.com/models/fcosr_x50_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/fcosr/fcosr_x50_3x_dota.yml) | +| [PP-YOLOE-R-s](./ppyoloe_r/README.md) | 73.82 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_s_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml) | +| [PP-YOLOE-R-s](./ppyoloe_r/README.md) | 79.42 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_s_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml) | +| [PP-YOLOE-R-m](./ppyoloe_r/README.md) | 77.64 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_m_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml) | +| [PP-YOLOE-R-m](./ppyoloe_r/README.md) | 79.71 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_m_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml) | +| [PP-YOLOE-R-l](./ppyoloe_r/README.md) | 78.14 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml) | +| [PP-YOLOE-R-l](./ppyoloe_r/README.md) | 80.02 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml) | +| [PP-YOLOE-R-x](./ppyoloe_r/README.md) | 78.28 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_x_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml) | +| [PP-YOLOE-R-x](./ppyoloe_r/README.md) | 80.73 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_x_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml) | **注意:** diff --git a/configs/rotate/README_en.md b/configs/rotate/README_en.md index ef5160ec9..fc746ae12 100644 --- a/configs/rotate/README_en.md +++ b/configs/rotate/README_en.md @@ -15,7 +15,15 @@ Rotated object detection is used to detect rectangular bounding boxes with angle | Model | mAP | Lr Scheduler | Angle | Aug | GPU Number | images/GPU | download | config | |:---:|:----:|:---------:|:-----:|:--------:|:-----:|:------------:|:-------:|:------:| | [S2ANet](./s2anet/README_en.md) | 73.84 | 2x | le135 | - | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/s2anet_alignconv_2x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/s2anet/s2anet_alignconv_2x_dota.yml) | -| [FCOSR](./fcosr/README_en.md) | 76.62 | 3x | oc | - | 4 | 4 | [model](https://paddledet.bj.bcebos.com/models/fcosr_x50_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/fcosr/fcosr_x50_3x_dota.yml) | +| [FCOSR](./fcosr/README_en.md) | 76.62 | 3x | oc | RR | 4 | 4 | [model](https://paddledet.bj.bcebos.com/models/fcosr_x50_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/fcosr/fcosr_x50_3x_dota.yml) | +| [PP-YOLOE-R-s](./ppyoloe_r/README_en.md) | 73.82 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_s_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml) | +| [PP-YOLOE-R-s](./ppyoloe_r/README_en.md) | 79.42 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_s_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml) | +| [PP-YOLOE-R-m](./ppyoloe_r/README_en.md) | 77.64 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_m_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml) | +| [PP-YOLOE-R-m](./ppyoloe_r/README_en.md) | 79.71 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_m_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml) | +| [PP-YOLOE-R-l](./ppyoloe_r/README_en.md) | 78.14 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml) | +| [PP-YOLOE-R-l](./ppyoloe_r/README_en.md) | 80.02 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml) | +| [PP-YOLOE-R-x](./ppyoloe_r/README_en.md) | 78.28 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_x_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml) | +| [PP-YOLOE-R-x](./ppyoloe_r/README_en.md) | 80.73 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_x_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml) | **Notes:** diff --git a/configs/rotate/fcosr/README.md b/configs/rotate/fcosr/README.md index 0113ee1f8..19888eea5 100644 --- a/configs/rotate/fcosr/README.md +++ b/configs/rotate/fcosr/README.md @@ -17,7 +17,7 @@ | 模型 | Backbone | mAP | 学习率策略 | 角度表示 | 数据增广 | GPU数目 | 每GPU图片数目 | 模型下载 | 配置文件 | |:---:|:--------:|:----:|:---------:|:-----:|:--------:|:-----:|:------------:|:-------:|:------:| -| FCOSR-M | ResNeXt-50 | 76.62 | 3x | oc | - | 4 | 4 | [model](https://paddledet.bj.bcebos.com/models/fcosr_x50_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/fcosr/fcosr_x50_3x_dota.yml) | +| FCOSR-M | ResNeXt-50 | 76.62 | 3x | oc | RR | 4 | 4 | [model](https://paddledet.bj.bcebos.com/models/fcosr_x50_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/fcosr/fcosr_x50_3x_dota.yml) | **注意:** diff --git a/configs/rotate/fcosr/README_en.md b/configs/rotate/fcosr/README_en.md index cf8e49ae4..ee16d0edb 100644 --- a/configs/rotate/fcosr/README_en.md +++ b/configs/rotate/fcosr/README_en.md @@ -17,7 +17,7 @@ English | [简体中文](README.md) | Model | Backbone | mAP | Lr Scheduler | Angle | Aug | GPU Number | images/GPU | download | config | |:---:|:--------:|:----:|:---------:|:-----:|:--------:|:-----:|:------------:|:-------:|:------:| -| FCOSR-M | ResNeXt-50 | 76.62 | 3x | oc | - | 4 | 4 | [model](https://paddledet.bj.bcebos.com/models/fcosr_x50_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/fcosr/fcosr_x50_3x_dota.yml) | +| FCOSR-M | ResNeXt-50 | 76.62 | 3x | oc | RR | 4 | 4 | [model](https://paddledet.bj.bcebos.com/models/fcosr_x50_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/fcosr/fcosr_x50_3x_dota.yml) | **Notes:** diff --git a/configs/rotate/ppyoloe_r/README.md b/configs/rotate/ppyoloe_r/README.md new file mode 100644 index 000000000..0892942ee --- /dev/null +++ b/configs/rotate/ppyoloe_r/README.md @@ -0,0 +1,147 @@ +简体中文 | [English](README_en.md) + +# PP-YOLOE-R + +## 内容 +- [简介](#简介) +- [模型库](#模型库) +- [使用说明](#使用说明) +- [预测部署](#预测部署) +- [附录](#附录) +- [引用](#引用) + +## 简介 +PP-YOLOE-R是一个高效的单阶段Anchor-free旋转框检测模型。基于PP-YOLOE, PP-YOLOE-R以极少的参数量和计算量为代价,引入了一系列有用的设计来提升检测精度。在DOTA 1.0数据集上,PP-YOLOE-R-l和PP-YOLOE-R-x在单尺度训练和测试的情况下分别达到了78.14和78.27 mAP,这超越了几乎所有的旋转框检测模型。通过多尺度训练和测试,PP-YOLOE-R-l和PP-YOLOE-R-x的检测精度进一步提升至80.02和80.73 mAP。在这种情况下,PP-YOLOE-R-x超越了所有的anchor-free方法并且和最先进的anchor-based的两阶段模型精度几乎相当。此外,PP-YOLOE-R-s和PP-YOLOE-R-m通过多尺度训练和测试可以达到79.42和79.71 mAP。考虑到这两个模型的参数量和计算量,其性能也非常卓越。在保持高精度的同时,PP-YOLOE-R避免使用特殊的算子,例如Deformable Convolution或Rotated RoI Align,以使其能轻松地部署在多种多样的硬件上。在1024x1024的输入分辨率下,PP-YOLOE-R-s/m/l/x在RTX 2080 Ti上使用TensorRT FP16分别能达到69.8/55.1/48.3/37.1 FPS,在Tesla V100上分别能达到114.5/86.8/69.7/50.7 FPS。更多细节可以参考我们的技术报告。 + +
+ +
+ +PP-YOLOE-R相较于PP-YOLOE做了以下几点改动: +- Rotated Task Alignment Learning +- 解耦的角度预测头 +- 使用DFL进行角度预测 +- 可学习的门控单元 +- [ProbIoU损失函数](https://arxiv.org/abs/2106.06072) + +## 模型库 + +| 模型 | Backbone | mAP | V100 TRT FP16 (FPS) | RTX 2080 Ti TRT FP16 (FPS) |学习率策略 | 角度表示 | 数据增广 | GPU数目 | 每GPU图片数目 | 模型下载 | 配置文件 | +|:---:|:--------:|:----:|:--------------------:|:------------:|:--------------------:|:-----:|:--------:|:-------:|:------:|:-----------:|:------:| +| PP-YOLOE-R-s | CRN-s | 73.82 | 114.5 | 69.8 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_s_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml) | +| PP-YOLOE-R-s | CRN-s | 79.42 | 114.5 | 69.8 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_s_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml) | +| PP-YOLOE-R-m | CRN-m | 77.64 | 86.8 | 55.1 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_m_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml) | +| PP-YOLOE-R-m | CRN-m | 79.71 | 86.8 | 55.1 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_m_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml) | +| PP-YOLOE-R-l | CRN-l | 78.14 | 69.7 | 48.3 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml) | +| PP-YOLOE-R-l | CRN-l | 80.02 | 69.7 | 48.3 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml) | +| PP-YOLOE-R-x | CRN-x | 78.28 | 50.7 | 37.1 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_x_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml) | +| PP-YOLOE-R-x | CRN-x | 80.73 | 50.7 | 37.1 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_x_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml) | + +**注意:** + +- 如果**GPU卡数**或者**batch size**发生了改变,你需要按照公式 **lrnew = lrdefault * (batch_sizenew * GPU_numbernew) / (batch_sizedefault * GPU_numberdefault)** 调整学习率。 +- 模型库中的模型默认使用单尺度训练单尺度测试。如果数据增广一栏标明MS,意味着使用多尺度训练和多尺度测试。如果数据增广一栏标明RR,意味着使用RandomRotate数据增广进行训练。 +- CRN表示在PP-YOLOE中提出的CSPRepResNet +- 速度测试使用TensorRT 8.2.3在DOTA测试集中测试2000张图片计算平均值得到。参考速度测试以复现[速度测试](#速度测试) + +## 使用说明 + +参考[数据准备](../README.md#数据准备)准备数据。 + +### 训练 + +GPU单卡训练 +``` bash +CUDA_VISIBLE_DEVICES=0 python tools/train.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml +``` + +GPU多卡训练 +``` bash +CUDA_VISIBLE_DEVICES=0,1,2,3 python -m paddle.distributed.launch --gpus 0,1,2,3 tools/train.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml +``` + +### 预测 + +执行以下命令预测单张图片,图片预测结果会默认保存在`output`文件夹下面 +``` bash +python tools/infer.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams --infer_img=demo/P0861__1.0__1154___824.png --draw_threshold=0.5 +``` + +### DOTA数据集评估 + +参考[DOTA Task](https://captain-whu.github.io/DOTA/tasks.html), 评估DOTA数据集需要生成一个包含所有检测结果的zip文件,每一类的检测结果储存在一个txt文件中,txt文件中每行格式为:`image_name score x1 y1 x2 y2 x3 y3 x4 y4`。将生成的zip文件提交到[DOTA Evaluation](https://captain-whu.github.io/DOTA/evaluation.html)的Task1进行评估。你可以执行以下命令得到test数据集的预测结果: +``` bash +python tools/infer.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams --infer_dir=/path/to/test/images --output_dir=output_ppyoloe_r --visualize=False --save_results=True +``` +将预测结果处理成官网评估所需要的格式: +``` bash +python configs/rotate/tools/generate_result.py --pred_txt_dir=output_ppyoloe_r/ --output_dir=submit/ --data_type=dota10 + +zip -r submit.zip submit +``` + +### 速度测试 +速度测试需要确保**TensorRT版本大于8.2, PaddlePaddle版本大于2.4.0rc0**。使用Paddle Inference且使用TensorRT进行测速,执行以下命令: + +``` bash +# 导出模型 +python tools/export_model.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams trt=True + +# 速度测试 +CUDA_VISIBLE_DEVICES=0 python configs/rotate/tools/inference_benchmark.py --model_dir output_inference/ppyoloe_r_crn_l_3x_dota/ --image_dir /path/to/dota/test/dir --run_mode trt_fp16 +``` + +## 预测部署 + +**使用Paddle Inference但不使用TensorRT**进行部署,执行以下命令: +``` bash +# 导出模型 +python tools/export_model.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams + +# 预测图片 +python deploy/python/infer.py --image_file demo/P0072__1.0__0___0.png --model_dir=output_inference/ppyoloe_r_crn_l_3x_dota --run_mode=paddle --device=gpu +``` + +**使用Paddle Inference且使用TensorRT**进行部署,执行以下命令: +``` +# 导出模型 +python tools/export_model.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams trt=True + +# 预测图片 +python deploy/python/infer.py --image_file demo/P0072__1.0__0___0.png --model_dir=output_inference/ppyoloe_r_crn_l_3x_dota --run_mode=trt_fp16 --device=gpu +``` + +**注意:** +- 使用Paddle-TRT使用确保PaddlePaddle版本大于2.4.0rc且TensorRT版本大于8.2. + + +## 附录 + +PP-YOLOE-R消融实验 + +| 模型 | mAP | 参数量(M) | FLOPs(G) | +| :-: | :-: | :------: | :------: | +| Baseline | 75.61 | 50.65 | 269.09 | +| +Rotated Task Alignment Learning | 77.24 | 50.65 | 269.09 | +| +Decoupled Angle Prediction Head | 77.78 | 52.20 | 272.72 | +| +Angle Prediction with DFL | 78.01 | 53.29 | 281.65 | +| +Learnable Gating Unit for RepVGG | 78.14 | 53.29 | 281.65 | + + +## 引用 + +``` +@article{xu2022pp, + title={PP-YOLOE: An evolved version of YOLO}, + author={Xu, Shangliang and Wang, Xinxin and Lv, Wenyu and Chang, Qinyao and Cui, Cheng and Deng, Kaipeng and Wang, Guanzhong and Dang, Qingqing and Wei, Shengyu and Du, Yuning and others}, + journal={arXiv preprint arXiv:2203.16250}, + year={2022} +} + +@article{llerena2021gaussian, + title={Gaussian Bounding Boxes and Probabilistic Intersection-over-Union for Object Detection}, + author={Llerena, Jeffri M and Zeni, Luis Felipe and Kristen, Lucas N and Jung, Claudio}, + journal={arXiv preprint arXiv:2106.06072}, + year={2021} +} +``` diff --git a/configs/rotate/ppyoloe_r/README_en.md b/configs/rotate/ppyoloe_r/README_en.md new file mode 100644 index 000000000..b98cc736f --- /dev/null +++ b/configs/rotate/ppyoloe_r/README_en.md @@ -0,0 +1,145 @@ +English | [简体中文](README.md) + +# PP-YOLOE-R + +## Content +- [Introduction](#Introduction) +- [Model Zoo](#Model-Zoo) +- [Getting Start](#Getting-Start) +- [Deployment](#Deployment) +- [Appendix](#Appendix) +- [Citations](#Citations) + +## Introduction +PP-YOLOE-R is an efficient anchor-free rotated object detector. Based on PP-YOLOE, PP-YOLOE-R introduces a bag of useful tricks to improve detection precision at the expense of marginal parameters and computations.PP-YOLOE-R-l and PP-YOLOE-R-x achieve 78.14 and 78.27 mAP respectively on DOTA 1.0 dataset with single-scale training and testing, which outperform almost all other rotated object detectors. With multi-scale training and testing, the detection precision of PP-YOLOE-R-l and PP-YOLOE-R-x is further improved to 80.02 and 80.73 mAP. In this case, PP-YOLOE-R-x surpasses all anchor-free methods and demonstrates competitive performance to state-of-the-art anchor-based two-stage model. Moreover, PP-YOLOE-R-s and PP-YOLOE-R-m can achieve 79.42 and 79.71 mAP with multi-scale training and testing, which is an excellent result considering the parameters and GLOPS of these two models. While maintaining high precision, PP-YOLOE-R avoids using special operators, such as Deformable Convolution or Rotated RoI Align, to be deployed friendly on various hardware. At the input resolution of 1024$\times$1024, PP-YOLOE-R-s/m/l/x can reach 69.8/55.1/48.3/37.1 FPS on RTX 2080 Ti and 114.5/86.8/69.7/50.7 FPS on Tesla V100 GPU with TensorRT and FP16-precision. For more details, please refer to our technical report. + +
+ +
+ +Compared with PP-YOLOE, PP-YOLOE-R has made the following changes: +- Rotated Task Alignment Learning +- Decoupled Angle Prediction Head +- Angle Prediction with DFL +- Learnable Gating Unit for RepVGG +- [ProbIoU Loss](https://arxiv.org/abs/2106.06072) + +## Model Zoo +| Model | Backbone | mAP | V100 TRT FP16 (FPS) | RTX 2080 Ti TRT FP16 (FPS) | Lr Scheduler | Angle | Aug | GPU Number | images/GPU | download | config | +|:---:|:--------:|:----:|:--------------------:|:------------:|:--------------------:|:-----:|:--------:|:-------:|:------:|:-----------:|:------:| +| PP-YOLOE-R-s | CRN-s | 73.82 | 114.5 | 69.8 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_s_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml) | +| PP-YOLOE-R-s | CRN-s | 79.42 | 114.5 | 69.8 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_s_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml) | +| PP-YOLOE-R-m | CRN-m | 77.64 | 86.8 | 55.1 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_m_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml) | +| PP-YOLOE-R-m | CRN-m | 79.71 | 86.8 | 55.1 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_m_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml) | +| PP-YOLOE-R-l | CRN-l | 78.14 | 69.7 | 48.3 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml) | +| PP-YOLOE-R-l | CRN-l | 80.02 | 69.7 | 48.3 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml) | +| PP-YOLOE-R-x | CRN-x | 78.28 | 50.7 | 37.1 | 3x | oc | RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_x_3x_dota.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml) | +| PP-YOLOE-R-x | CRN-x | 80.73 | 50.7 | 37.1 | 3x | oc | MS+RR | 4 | 2 | [model](https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_x_3x_dota_ms.pdparams) | [config](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml) | + +**Notes:** + +- if **GPU number** or **mini-batch size** is changed, **learning rate** should be adjusted according to the formula **lrnew = lrdefault * (batch_sizenew * GPU_numbernew) / (batch_sizedefault * GPU_numberdefault)**. +- Models in model zoo is trained and tested with single scale by default. If `MS` is indicated in the data augmentation column, it means that multi-scale training and multi-scale testing are used. If `RR` is indicated in the data augmentation column, it means that RandomRotate data augmentation is used for training. +- CRN denotes CSPRepResNet proposed in PP-YOLOE +- Speed ​​is calculated and averaged by testing 2000 images on the DOTA test dataset. Refer to [Speed testing](#Speed-testing) to reproduce the results. + +## Getting Start + +Refer to [Data-Preparation](../README_en.md#Data-Preparation) to prepare data. + +### Training + +Single GPU Training +``` bash +CUDA_VISIBLE_DEVICES=0 python tools/train.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml +``` + +Multiple GPUs Training +``` bash +CUDA_VISIBLE_DEVICES=0,1,2,3 python -m paddle.distributed.launch --gpus 0,1,2,3 tools/train.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml +``` + +### Inference + +Run the follow command to infer single image, the result of inference will be saved in `output` directory by default. + +``` bash +python tools/infer.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams --infer_img=demo/P0861__1.0__1154___824.png --draw_threshold=0.5 +``` + +### Evaluation on DOTA Dataset +Refering to [DOTA Task](https://captain-whu.github.io/DOTA/tasks.html), You need to submit a zip file containing results for all test images for evaluation. The detection results of each category are stored in a txt file, each line of which is in the following format +`image_id score x1 y1 x2 y2 x3 y3 x4 y4`. To evaluate, you should submit the generated zip file to the Task1 of [DOTA Evaluation](https://captain-whu.github.io/DOTA/evaluation.html). You can run the following command to get the inference results of test dataset: +``` bash +python tools/infer.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams --infer_dir=/path/to/test/images --output_dir=output_ppyoloe_r --visualize=False --save_results=True +``` +Process the prediction results into the format required for the official website evaluation: +``` bash +python configs/rotate/tools/generate_result.py --pred_txt_dir=output_ppyoloe_r/ --output_dir=submit/ --data_type=dota10 + +zip -r submit.zip submit +``` + +### Speed testing + +To test speed, make sure that **the version of TensorRT is larger than 8.2 and the version of PaddlePaddle is larger than 2.4.0rc**. Using Paddle Inference with TensorRT to test speed, run following command + +``` bash +# export inference model +python tools/export_model.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams trt=True + +# speed testing +CUDA_VISIBLE_DEVICES=0 python configs/rotate/tools/inference_benchmark.py --model_dir output_inference/ppyoloe_r_crn_l_3x_dota/ --image_dir /path/to/dota/test/dir --run_mode trt_fp16 +``` + +## Deployment + +**Using Paddle Inference without TensorRT** to for deployment, run following command + +``` bash +# export inference model +python tools/export_model.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams + +# inference single image +python deploy/python/infer.py --image_file demo/P0072__1.0__0___0.png --model_dir=output_inference/ppyoloe_r_crn_l_3x_dota --run_mode=paddle --device=gpu +``` + +**Using Paddle Inference with TensorRT** to for deployment, run following command + +``` bash +# export inference model +python tools/export_model.py -c configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_r_crn_l_3x_dota.pdparams trt=True + +# inference single image +python deploy/python/infer.py --image_file demo/P0072__1.0__0___0.png --model_dir=output_inference/ppyoloe_r_crn_l_3x_dota --run_mode=trt_fp16 --device=gpu +``` + +## Appendix + +Ablation experiments of PP-YOLOE-R + +| Model | mAP | Params(M) | FLOPs(G) | +| :-: | :-: | :------: | :------: | +| Baseline | 75.61 | 50.65 | 269.09 | +| +Rotated Task Alignment Learning | 77.24 | 50.65 | 269.09 | +| +Decoupled Angle Prediction Head | 77.78 | 52.20 | 272.72 | +| +Angle Prediction with DFL | 78.01 | 53.29 | 281.65 | +| +Learnable Gating Unit for RepVGG | 78.14 | 53.29 | 281.65 | + +## Citations + +``` +@article{xu2022pp, + title={PP-YOLOE: An evolved version of YOLO}, + author={Xu, Shangliang and Wang, Xinxin and Lv, Wenyu and Chang, Qinyao and Cui, Cheng and Deng, Kaipeng and Wang, Guanzhong and Dang, Qingqing and Wei, Shengyu and Du, Yuning and others}, + journal={arXiv preprint arXiv:2203.16250}, + year={2022} +} + +@article{llerena2021gaussian, + title={Gaussian Bounding Boxes and Probabilistic Intersection-over-Union for Object Detection}, + author={Llerena, Jeffri M and Zeni, Luis Felipe and Kristen, Lucas N and Jung, Claudio}, + journal={arXiv preprint arXiv:2106.06072}, + year={2021} +} +``` diff --git a/configs/rotate/ppyoloe_r/_base_/optimizer_3x.yml b/configs/rotate/ppyoloe_r/_base_/optimizer_3x.yml new file mode 100644 index 000000000..1cdad4beb --- /dev/null +++ b/configs/rotate/ppyoloe_r/_base_/optimizer_3x.yml @@ -0,0 +1,19 @@ +epoch: 36 + +LearningRate: + base_lr: 0.008 + schedulers: + - !CosineDecay + max_epochs: 44 + - !LinearWarmup + start_factor: 0. + steps: 1000 + +OptimizerBuilder: + clip_grad_by_norm: 35. + optimizer: + momentum: 0.9 + type: Momentum + regularizer: + factor: 0.0005 + type: L2 diff --git a/configs/rotate/ppyoloe_r/_base_/ppyoloe_r_crn.yml b/configs/rotate/ppyoloe_r/_base_/ppyoloe_r_crn.yml new file mode 100644 index 000000000..ab5bdb50a --- /dev/null +++ b/configs/rotate/ppyoloe_r/_base_/ppyoloe_r_crn.yml @@ -0,0 +1,49 @@ +architecture: YOLOv3 +norm_type: sync_bn +use_ema: true +ema_decay: 0.9998 + +YOLOv3: + backbone: CSPResNet + neck: CustomCSPPAN + yolo_head: PPYOLOERHead + post_process: ~ + +CSPResNet: + layers: [3, 6, 6, 3] + channels: [64, 128, 256, 512, 1024] + return_idx: [1, 2, 3] + use_large_stem: True + use_alpha: True + +CustomCSPPAN: + out_channels: [768, 384, 192] + stage_num: 1 + block_num: 3 + act: 'swish' + spp: true + use_alpha: True + +PPYOLOERHead: + fpn_strides: [32, 16, 8] + grid_cell_offset: 0.5 + use_varifocal_loss: true + static_assigner_epoch: -1 + loss_weight: {class: 1.0, iou: 2.5, dfl: 0.05} + static_assigner: + name: FCOSRAssigner + factor: 12 + threshold: 0.23 + boundary: [[512, 10000], [256, 512], [-1, 256]] + assigner: + name: RotatedTaskAlignedAssigner + topk: 13 + alpha: 1.0 + beta: 6.0 + nms: + name: MultiClassNMS + nms_top_k: 2000 + keep_top_k: -1 + score_threshold: 0.1 + nms_threshold: 0.1 + normalized: False diff --git a/configs/rotate/ppyoloe_r/_base_/ppyoloe_r_reader.yml b/configs/rotate/ppyoloe_r/_base_/ppyoloe_r_reader.yml new file mode 100644 index 000000000..aa9de88b6 --- /dev/null +++ b/configs/rotate/ppyoloe_r/_base_/ppyoloe_r_reader.yml @@ -0,0 +1,45 @@ +worker_num: 4 +image_height: &image_height 1024 +image_width: &image_width 1024 +image_size: &image_size [*image_height, *image_width] + +TrainReader: + sample_transforms: + - Decode: {} + - Poly2Array: {} + - RandomRFlip: {} + - RandomRRotate: {angle_mode: 'value', angle: [0, 90, 180, -90]} + - RandomRRotate: {angle_mode: 'value', angle: [30, 60], rotate_prob: 0.5} + - RResize: {target_size: *image_size, keep_ratio: True, interp: 2} + - Poly2RBox: {filter_threshold: 2, filter_mode: 'edge', rbox_type: 'oc'} + batch_transforms: + - NormalizeImage: {mean: [0.485, 0.456, 0.406], std: [0.229, 0.224, 0.225], is_scale: True} + - Permute: {} + - PadRGT: {} + - PadBatch: {pad_to_stride: 32} + batch_size: 2 + shuffle: true + drop_last: true + use_shared_memory: true + collate_batch: true + +EvalReader: + sample_transforms: + - Decode: {} + - Poly2Array: {} + - RResize: {target_size: *image_size, keep_ratio: True, interp: 2} + - NormalizeImage: {mean: [0.485, 0.456, 0.406], std: [0.229, 0.224, 0.225], is_scale: True} + - Permute: {} + batch_transforms: + - PadBatch: {pad_to_stride: 32} + batch_size: 2 + +TestReader: + sample_transforms: + - Decode: {} + - Resize: {target_size: *image_size, keep_ratio: True, interp: 2} + - NormalizeImage: {mean: [0.485, 0.456, 0.406], std: [0.229, 0.224, 0.225], is_scale: True} + - Permute: {} + batch_transforms: + - PadBatch: {pad_to_stride: 32} + batch_size: 8 diff --git a/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml new file mode 100644 index 000000000..b019d736c --- /dev/null +++ b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota.yml @@ -0,0 +1,15 @@ +_BASE_: [ + '../../datasets/dota.yml', + '../../runtime.yml', + '_base_/optimizer_3x.yml', + '_base_/ppyoloe_r_reader.yml', + '_base_/ppyoloe_r_crn.yml' +] + +log_iter: 50 +snapshot_epoch: 1 +weights: output/ppyoloe_r_crn_l_3x_dota/model_final + +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/CSPResNetb_l_pretrained.pdparams +depth_mult: 1.0 +width_mult: 1.0 diff --git a/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml new file mode 100644 index 000000000..a1411a315 --- /dev/null +++ b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_l_3x_dota_ms.yml @@ -0,0 +1,15 @@ +_BASE_: [ + '../../datasets/dota_ms.yml', + '../../runtime.yml', + '_base_/optimizer_3x.yml', + '_base_/ppyoloe_r_reader.yml', + '_base_/ppyoloe_r_crn.yml' +] + +log_iter: 50 +snapshot_epoch: 1 +weights: output/ppyoloe_r_crn_l_3x_dota/model_final + +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/CSPResNetb_l_pretrained.pdparams +depth_mult: 1.0 +width_mult: 1.0 diff --git a/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml new file mode 100644 index 000000000..755cf3f4e --- /dev/null +++ b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota.yml @@ -0,0 +1,15 @@ +_BASE_: [ + '../../datasets/dota.yml', + '../../runtime.yml', + '_base_/optimizer_3x.yml', + '_base_/ppyoloe_r_reader.yml', + '_base_/ppyoloe_r_crn.yml' +] + +log_iter: 50 +snapshot_epoch: 1 +weights: output/ppyoloe_r_crn_m_3x_dota/model_final + +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/CSPResNetb_m_pretrained.pdparams +depth_mult: 0.67 +width_mult: 0.75 diff --git a/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml new file mode 100644 index 000000000..d885b459f --- /dev/null +++ b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_m_3x_dota_ms.yml @@ -0,0 +1,15 @@ +_BASE_: [ + '../../datasets/dota_ms.yml', + '../../runtime.yml', + '_base_/optimizer_3x.yml', + '_base_/ppyoloe_r_reader.yml', + '_base_/ppyoloe_r_crn.yml' +] + +log_iter: 50 +snapshot_epoch: 1 +weights: output/ppyoloe_r_crn_m_3x_dota/model_final + +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/CSPResNetb_m_pretrained.pdparams +depth_mult: 0.67 +width_mult: 0.75 diff --git a/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml new file mode 100644 index 000000000..a227f18ac --- /dev/null +++ b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota.yml @@ -0,0 +1,15 @@ +_BASE_: [ + '../../datasets/dota.yml', + '../../runtime.yml', + '_base_/optimizer_3x.yml', + '_base_/ppyoloe_r_reader.yml', + '_base_/ppyoloe_r_crn.yml' +] + +log_iter: 50 +snapshot_epoch: 1 +weights: output/ppyoloe_r_crn_s_3x_dota/model_final + +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/CSPResNetb_s_pretrained.pdparams +depth_mult: 0.33 +width_mult: 0.50 diff --git a/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml new file mode 100644 index 000000000..921a9d571 --- /dev/null +++ b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_s_3x_dota_ms.yml @@ -0,0 +1,15 @@ +_BASE_: [ + '../../datasets/dota_ms.yml', + '../../runtime.yml', + '_base_/optimizer_3x.yml', + '_base_/ppyoloe_r_reader.yml', + '_base_/ppyoloe_r_crn.yml' +] + +log_iter: 50 +snapshot_epoch: 1 +weights: output/ppyoloe_r_crn_s_3x_dota/model_final + +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/CSPResNetb_s_pretrained.pdparams +depth_mult: 0.33 +width_mult: 0.50 diff --git a/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml new file mode 100644 index 000000000..d81b5ef98 --- /dev/null +++ b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota.yml @@ -0,0 +1,15 @@ +_BASE_: [ + '../../datasets/dota.yml', + '../../runtime.yml', + '_base_/optimizer_3x.yml', + '_base_/ppyoloe_r_reader.yml', + '_base_/ppyoloe_r_crn.yml' +] + +log_iter: 50 +snapshot_epoch: 1 +weights: output/ppyoloe_r_crn_x_3x_dota/model_final + +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/CSPResNetb_x_pretrained.pdparams +depth_mult: 1.33 +width_mult: 1.25 diff --git a/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml new file mode 100644 index 000000000..d99cdb078 --- /dev/null +++ b/configs/rotate/ppyoloe_r/ppyoloe_r_crn_x_3x_dota_ms.yml @@ -0,0 +1,15 @@ +_BASE_: [ + '../../datasets/dota_ms.yml', + '../../runtime.yml', + '_base_/optimizer_3x.yml', + '_base_/ppyoloe_r_reader.yml', + '_base_/ppyoloe_r_crn.yml' +] + +log_iter: 50 +snapshot_epoch: 1 +weights: output/ppyoloe_r_crn_x_3x_dota/model_final + +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/CSPResNetb_x_pretrained.pdparams +depth_mult: 1.33 +width_mult: 1.25 diff --git a/configs/rotate/tools/inference_benchmark.py b/configs/rotate/tools/inference_benchmark.py new file mode 100644 index 000000000..dcce2d2fe --- /dev/null +++ b/configs/rotate/tools/inference_benchmark.py @@ -0,0 +1,356 @@ +# Copyright (c) 2022 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 + +import os +import sys +import six +import glob +import time +import yaml +import argparse +import cv2 +import numpy as np + +import paddle +import paddle.version as paddle_version +from paddle.inference import Config, create_predictor, PrecisionType, get_trt_runtime_version + + +TUNED_TRT_DYNAMIC_MODELS = {'DETR'} + +def check_version(version='2.2'): + err = "PaddlePaddle version {} or higher is required, " \ + "or a suitable develop version is satisfied as well. \n" \ + "Please make sure the version is good with your code.".format(version) + + version_installed = [ + paddle_version.major, paddle_version.minor, paddle_version.patch, + paddle_version.rc + ] + + if version_installed == ['0', '0', '0', '0']: + return + + version_split = version.split('.') + + length = min(len(version_installed), len(version_split)) + for i in six.moves.range(length): + if version_installed[i] > version_split[i]: + return + if version_installed[i] < version_split[i]: + raise Exception(err) + + +def check_trt_version(version='8.2'): + err = "TensorRT version {} or higher is required," \ + "Please make sure the version is good with your code.".format(version) + version_split = list(map(int, version.split('.'))) + version_installed = get_trt_runtime_version() + length = min(len(version_installed), len(version_split)) + for i in six.moves.range(length): + if version_installed[i] > version_split[i]: + return + if version_installed[i] < version_split[i]: + raise Exception(err) + + +# preprocess ops +def decode_image(im_file, im_info): + if isinstance(im_file, str): + with open(im_file, 'rb') as f: + im_read = f.read() + data = np.frombuffer(im_read, dtype='uint8') + im = cv2.imdecode(data, 1) # BGR mode, but need RGB mode + im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB) + else: + im = im_file + im_info['im_shape'] = np.array(im.shape[:2], dtype=np.float32) + im_info['scale_factor'] = np.array([1., 1.], dtype=np.float32) + return im, im_info + +class Resize(object): + + def __init__(self, target_size, keep_ratio=True, interp=cv2.INTER_LINEAR): + if isinstance(target_size, int): + target_size = [target_size, target_size] + self.target_size = target_size + self.keep_ratio = keep_ratio + self.interp = interp + + def __call__(self, im, im_info): + assert len(self.target_size) == 2 + assert self.target_size[0] > 0 and self.target_size[1] > 0 + im_channel = im.shape[2] + im_scale_y, im_scale_x = self.generate_scale(im) + im = cv2.resize( + im, + None, + None, + fx=im_scale_x, + fy=im_scale_y, + interpolation=self.interp) + im_info['im_shape'] = np.array(im.shape[:2]).astype('float32') + im_info['scale_factor'] = np.array( + [im_scale_y, im_scale_x]).astype('float32') + return im, im_info + + def generate_scale(self, im): + origin_shape = im.shape[:2] + im_c = im.shape[2] + if self.keep_ratio: + im_size_min = np.min(origin_shape) + im_size_max = np.max(origin_shape) + target_size_min = np.min(self.target_size) + target_size_max = np.max(self.target_size) + im_scale = float(target_size_min) / float(im_size_min) + if np.round(im_scale * im_size_max) > target_size_max: + im_scale = float(target_size_max) / float(im_size_max) + im_scale_x = im_scale + im_scale_y = im_scale + else: + resize_h, resize_w = self.target_size + im_scale_y = resize_h / float(origin_shape[0]) + im_scale_x = resize_w / float(origin_shape[1]) + return im_scale_y, im_scale_x + +class Permute(object): + + def __init__(self, ): + super(Permute, self).__init__() + + def __call__(self, im, im_info): + im = im.transpose((2, 0, 1)) + return im, im_info + +class NormalizeImage(object): + def __init__(self, mean, std, is_scale=True, norm_type='mean_std'): + self.mean = mean + self.std = std + self.is_scale = is_scale + self.norm_type = norm_type + + def __call__(self, im, im_info): + im = im.astype(np.float32, copy=False) + if self.is_scale: + scale = 1.0 / 255.0 + im *= scale + + if self.norm_type == 'mean_std': + mean = np.array(self.mean)[np.newaxis, np.newaxis, :] + std = np.array(self.std)[np.newaxis, np.newaxis, :] + im -= mean + im /= std + return im, im_info + + +class PadStride(object): + + def __init__(self, stride=0): + self.coarsest_stride = stride + + def __call__(self, im, im_info): + coarsest_stride = self.coarsest_stride + if coarsest_stride <= 0: + return im, im_info + im_c, im_h, im_w = im.shape + pad_h = int(np.ceil(float(im_h) / coarsest_stride) * coarsest_stride) + pad_w = int(np.ceil(float(im_w) / coarsest_stride) * coarsest_stride) + padding_im = np.zeros((im_c, pad_h, pad_w), dtype=np.float32) + padding_im[:, :im_h, :im_w] = im + return padding_im, im_info + + +def preprocess(im, preprocess_ops): + # process image by preprocess_ops + im_info = { + 'scale_factor': np.array( + [1., 1.], dtype=np.float32), + 'im_shape': None, + } + im, im_info = decode_image(im, im_info) + for operator in preprocess_ops: + im, im_info = operator(im, im_info) + return im, im_info + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--model_dir', type=str, help='directory of inference model') + parser.add_argument('--run_mode', type=str, default='paddle', help='running mode') + parser.add_argument('--batch_size', type=int, default=1, help='batch size') + parser.add_argument('--image_dir', type=str, default='/paddle/data/DOTA_1024_ss/test1024/images', help='directory of test images') + parser.add_argument('--warmup_iter', type=int, default=5, help='num of warmup iters') + parser.add_argument('--total_iter', type=int, default=2000, help='num of total iters') + parser.add_argument('--log_iter', type=int, default=50, help='num of log interval') + parser.add_argument('--tuned_trt_shape_file', type=str, default='shape_range_info.pbtxt', help='dynamic shape range info') + args = parser.parse_args() + return args + + +def init_predictor(FLAGS): + model_dir, run_mode, batch_size = FLAGS.model_dir, FLAGS.run_mode, FLAGS.batch_size + yaml_file = os.path.join(model_dir, 'infer_cfg.yml') + with open(yaml_file) as f: + yml_conf = yaml.safe_load(f) + + config = Config( + os.path.join(model_dir, 'model.pdmodel'), + os.path.join(model_dir, 'model.pdiparams')) + + # initial GPU memory(M), device ID + config.enable_use_gpu(200, 0) + # optimize graph and fuse op + config.switch_ir_optim(True) + + precision_map = { + 'trt_int8': Config.Precision.Int8, + 'trt_fp32': Config.Precision.Float32, + 'trt_fp16': Config.Precision.Half + } + + arch = yml_conf['arch'] + tuned_trt_shape_file = os.path.join(model_dir, FLAGS.tuned_trt_shape_file) + + if run_mode in precision_map.keys(): + if arch in TUNED_TRT_DYNAMIC_MODELS and not os.path.exists(tuned_trt_shape_file): + print('dynamic shape range info is saved in {}. After that, rerun the code'.format(tuned_trt_shape_file)) + config.collect_shape_range_info(tuned_trt_shape_file) + config.enable_tensorrt_engine( + workspace_size=(1 << 25) * batch_size, + max_batch_size=batch_size, + min_subgraph_size=yml_conf['min_subgraph_size'], + precision_mode=precision_map[run_mode], + use_static=True, + use_calib_mode=False) + + if yml_conf['use_dynamic_shape']: + if arch in TUNED_TRT_DYNAMIC_MODELS and os.path.exists(tuned_trt_shape_file): + config.enable_tuned_tensorrt_dynamic_shape(tuned_trt_shape_file, True) + else: + min_input_shape = { + 'image': [batch_size, 3, 640, 640], + 'scale_factor': [batch_size, 2] + } + max_input_shape = { + 'image': [batch_size, 3, 1280, 1280], + 'scale_factor': [batch_size, 2] + } + opt_input_shape = { + 'image': [batch_size, 3, 1024, 1024], + 'scale_factor': [batch_size, 2] + } + config.set_trt_dynamic_shape_info(min_input_shape, max_input_shape, + opt_input_shape) + + # disable print log when predict + config.disable_glog_info() + # enable shared memory + config.enable_memory_optim() + # disable feed, fetch OP, needed by zero_copy_run + config.switch_use_feed_fetch_ops(False) + predictor = create_predictor(config) + return predictor, yml_conf + +def create_preprocess_ops(yml_conf): + preprocess_ops = [] + for op_info in yml_conf['Preprocess']: + new_op_info = op_info.copy() + op_type = new_op_info.pop('type') + preprocess_ops.append(eval(op_type)(**new_op_info)) + return preprocess_ops + + +def get_test_images(image_dir): + images = set() + infer_dir = os.path.abspath(image_dir) + exts = ['jpg', 'jpeg', 'png', 'bmp'] + exts += [ext.upper() for ext in exts] + for ext in exts: + images.update(glob.glob('{}/*.{}'.format(infer_dir, ext))) + images = list(images) + return images + + +def create_inputs(image_files, preprocess_ops): + inputs = dict() + im_list, im_info_list = [], [] + for im_path in image_files: + im, im_info = preprocess(im_path, preprocess_ops) + im_list.append(im) + im_info_list.append(im_info) + + inputs['im_shape'] = np.stack([e['im_shape'] for e in im_info_list], axis=0).astype('float32') + inputs['scale_factor'] = np.stack([e['scale_factor'] for e in im_info_list], axis=0).astype('float32') + inputs['image'] = np.stack(im_list, axis=0).astype('float32') + return inputs + + +def measure_speed(FLAGS): + predictor, yml_conf = init_predictor(FLAGS) + input_names = predictor.get_input_names() + preprocess_ops = create_preprocess_ops(yml_conf) + + image_files = get_test_images(FLAGS.image_dir) + + batch_size = FLAGS.batch_size + warmup_iter, log_iter, total_iter = FLAGS.warmup_iter, FLAGS.log_iter, FLAGS.total_iter + + total_time = 0 + fps = 0 + for i in range(0, total_iter, batch_size): + # make data ready + inputs = create_inputs(image_files[i:i + batch_size], preprocess_ops) + for name in input_names: + input_tensor = predictor.get_input_handle(name) + input_tensor.copy_from_cpu(inputs[name]) + + paddle.device.cuda.synchronize() + # start running + start_time = time.perf_counter() + predictor.run() + paddle.device.cuda.synchronize() + + if i >= warmup_iter: + total_time += time.perf_counter() - start_time + if (i + 1) % log_iter == 0: + fps = (i + 1 - warmup_iter) / total_time + print( + f'Done image [{i + 1:<3}/ {total_iter}], ' + f'fps: {fps:.1f} img / s, ' + f'times per image: {1000 / fps:.1f} ms / img', + flush=True) + + if (i + 1) == total_iter: + fps = (i + 1 - warmup_iter) / total_time + print( + f'Overall fps: {fps:.1f} img / s, ' + f'times per image: {1000 / fps:.1f} ms / img', + flush=True) + break + +if __name__ == '__main__': + FLAGS = parse_args() + check_version('2.4') + check_trt_version('8.2') + measure_speed(FLAGS) + + + + + + diff --git a/docs/MODEL_ZOO_cn.md b/docs/MODEL_ZOO_cn.md index 50a835b50..7235b009d 100644 --- a/docs/MODEL_ZOO_cn.md +++ b/docs/MODEL_ZOO_cn.md @@ -110,9 +110,7 @@ Paddle提供基于ImageNet的骨架网络预训练模型。所有预训练模型 ## 旋转框检测 -### S2ANet - -请参考[S2ANet](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/dota/) +[旋转框检测模型库](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate) ## 关键点检测 diff --git a/docs/MODEL_ZOO_en.md b/docs/MODEL_ZOO_en.md index 599121ac0..c26e5c1db 100644 --- a/docs/MODEL_ZOO_en.md +++ b/docs/MODEL_ZOO_en.md @@ -107,12 +107,9 @@ Please refer to[YOLOv6](https://github.com/nemonameless/PaddleDetection_YOLOSeri Please refer to[YOLOv7](https://github.com/nemonameless/PaddleDetection_YOLOSeries/tree/develop/configs/yolov7) -## Rotating frame detection - -### S2ANet - -Please refer to[S2ANet](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/dota/) +## Rotated Object detection +[Model Zoo for Rotated Object Detection](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/rotate) ## KeyPoint Detection diff --git a/docs/images/ppyoloe_r_map_fps.png b/docs/images/ppyoloe_r_map_fps.png new file mode 100644 index 0000000000000000000000000000000000000000..2d4553b97e96a63c428b08a2da9d0f8880e72be8 GIT binary patch literal 148038 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 literal 0 HcmV?d00001 diff --git a/ppdet/ext_op/csrc/rbox_iou/matched_rbox_iou_op.cc b/ppdet/ext_op/csrc/matched_rbox_iou/matched_rbox_iou.cc similarity index 68% rename from ppdet/ext_op/csrc/rbox_iou/matched_rbox_iou_op.cc rename to ppdet/ext_op/csrc/matched_rbox_iou/matched_rbox_iou.cc index 2c3c58b60..b16e8c1f2 100644 --- a/ppdet/ext_op/csrc/rbox_iou/matched_rbox_iou_op.cc +++ b/ppdet/ext_op/csrc/matched_rbox_iou/matched_rbox_iou.cc @@ -13,14 +13,14 @@ // limitations under the License. // // The code is based on -// https://github.com/csuhan/s2anet/blob/master/mmdet/ops/box_iou_rotated +// https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/csrc/box_iou_rotated/ +#include "../rbox_iou/rbox_iou_utils.h" #include "paddle/extension.h" -#include "rbox_iou_op.h" template void matched_rbox_iou_cpu_kernel(const int rbox_num, const T *rbox1_data_ptr, - const T *rbox2_data_ptr, T *output_data_ptr) { + const T *rbox2_data_ptr, T *output_data_ptr) { int i; for (i = 0; i < rbox_num; i++) { @@ -30,42 +30,43 @@ void matched_rbox_iou_cpu_kernel(const int rbox_num, const T *rbox1_data_ptr, } #define CHECK_INPUT_CPU(x) \ - PD_CHECK(x.place() == paddle::PlaceType::kCPU, #x " must be a CPU Tensor.") + PD_CHECK(x.is_cpu(), #x " must be a CPU Tensor.") -std::vector MatchedRboxIouCPUForward(const paddle::Tensor &rbox1, - const paddle::Tensor &rbox2) { +std::vector +MatchedRboxIouCPUForward(const paddle::Tensor &rbox1, + const paddle::Tensor &rbox2) { CHECK_INPUT_CPU(rbox1); CHECK_INPUT_CPU(rbox2); PD_CHECK(rbox1.shape()[0] == rbox2.shape()[0], "inputs must be same dim"); auto rbox_num = rbox1.shape()[0]; - auto output = paddle::Tensor(paddle::PlaceType::kCPU, {rbox_num}); + auto output = paddle::empty({rbox_num}, rbox1.dtype(), paddle::CPUPlace()); - PD_DISPATCH_FLOATING_TYPES(rbox1.type(), "rotated_iou_cpu_kernel", ([&] { + PD_DISPATCH_FLOATING_TYPES(rbox1.type(), "matched_rbox_iou_cpu_kernel", ([&] { matched_rbox_iou_cpu_kernel( rbox_num, rbox1.data(), - rbox2.data(), - output.mutable_data()); + rbox2.data(), output.data()); })); return {output}; } #ifdef PADDLE_WITH_CUDA -std::vector MatchedRboxIouCUDAForward(const paddle::Tensor &rbox1, - const paddle::Tensor &rbox2); +std::vector +MatchedRboxIouCUDAForward(const paddle::Tensor &rbox1, + const paddle::Tensor &rbox2); #endif #define CHECK_INPUT_SAME(x1, x2) \ PD_CHECK(x1.place() == x2.place(), "input must be smae pacle.") std::vector MatchedRboxIouForward(const paddle::Tensor &rbox1, - const paddle::Tensor &rbox2) { + const paddle::Tensor &rbox2) { CHECK_INPUT_SAME(rbox1, rbox2); - if (rbox1.place() == paddle::PlaceType::kCPU) { + if (rbox1.is_cpu()) { return MatchedRboxIouCPUForward(rbox1, rbox2); #ifdef PADDLE_WITH_CUDA - } else if (rbox1.place() == paddle::PlaceType::kGPU) { + } else if (rbox1.is_gpu()) { return MatchedRboxIouCUDAForward(rbox1, rbox2); #endif } @@ -73,12 +74,12 @@ std::vector MatchedRboxIouForward(const paddle::Tensor &rbox1, std::vector> MatchedRboxIouInferShape(std::vector rbox1_shape, - std::vector rbox2_shape) { + std::vector rbox2_shape) { return {{rbox1_shape[0]}}; } std::vector MatchedRboxIouInferDtype(paddle::DataType t1, - paddle::DataType t2) { + paddle::DataType t2) { return {t1}; } diff --git a/ppdet/ext_op/csrc/rbox_iou/matched_rbox_iou_op.cu b/ppdet/ext_op/csrc/matched_rbox_iou/matched_rbox_iou.cu similarity index 72% rename from ppdet/ext_op/csrc/rbox_iou/matched_rbox_iou_op.cu rename to ppdet/ext_op/csrc/matched_rbox_iou/matched_rbox_iou.cu index 8d03ecce6..53454d106 100644 --- a/ppdet/ext_op/csrc/rbox_iou/matched_rbox_iou_op.cu +++ b/ppdet/ext_op/csrc/matched_rbox_iou/matched_rbox_iou.cu @@ -13,21 +13,15 @@ // limitations under the License. // // The code is based on -// https://github.com/csuhan/s2anet/blob/master/mmdet/ops/box_iou_rotated +// https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/csrc/box_iou_rotated/ +#include "../rbox_iou/rbox_iou_utils.h" #include "paddle/extension.h" -#include "rbox_iou_op.h" - -/** - Computes ceil(a / b) -*/ - -static inline int CeilDiv(const int a, const int b) { return (a + b - 1) / b; } template __global__ void matched_rbox_iou_cuda_kernel(const int rbox_num, const T *rbox1_data_ptr, - const T *rbox2_data_ptr, T *output_data_ptr) { + const T *rbox2_data_ptr, T *output_data_ptr) { for (int tid = blockIdx.x * blockDim.x + threadIdx.x; tid < rbox_num; tid += blockDim.x * gridDim.x) { output_data_ptr[tid] = @@ -36,17 +30,18 @@ matched_rbox_iou_cuda_kernel(const int rbox_num, const T *rbox1_data_ptr, } #define CHECK_INPUT_GPU(x) \ - PD_CHECK(x.place() == paddle::PlaceType::kGPU, #x " must be a GPU Tensor.") + PD_CHECK(x.is_gpu(), #x " must be a GPU Tensor.") -std::vector MatchedRboxIouCUDAForward(const paddle::Tensor &rbox1, - const paddle::Tensor &rbox2) { +std::vector +MatchedRboxIouCUDAForward(const paddle::Tensor &rbox1, + const paddle::Tensor &rbox2) { CHECK_INPUT_GPU(rbox1); CHECK_INPUT_GPU(rbox2); PD_CHECK(rbox1.shape()[0] == rbox2.shape()[0], "inputs must be same dim"); auto rbox_num = rbox1.shape()[0]; - auto output = paddle::Tensor(paddle::PlaceType::kGPU, {rbox_num}); + auto output = paddle::empty({rbox_num}, rbox1.dtype(), paddle::GPUPlace()); const int thread_per_block = 512; const int block_per_grid = CeilDiv(rbox_num, thread_per_block); @@ -56,7 +51,7 @@ std::vector MatchedRboxIouCUDAForward(const paddle::Tensor &rbox matched_rbox_iou_cuda_kernel< data_t><<>>( rbox_num, rbox1.data(), rbox2.data(), - output.mutable_data()); + output.data()); })); return {output}; diff --git a/ppdet/ext_op/csrc/nms_rotated/nms_rotated.cc b/ppdet/ext_op/csrc/nms_rotated/nms_rotated.cc new file mode 100644 index 000000000..44f4eb62b --- /dev/null +++ b/ppdet/ext_op/csrc/nms_rotated/nms_rotated.cc @@ -0,0 +1,121 @@ +// Copyright (c) 2022 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 "../rbox_iou/rbox_iou_utils.h" +#include "paddle/extension.h" + +template +void nms_rotated_cpu_kernel(const T *boxes_data, const float threshold, + const int64_t num_boxes, int64_t *num_keep_boxes, + int64_t *output_data) { + + int num_masks = CeilDiv(num_boxes, 64); + std::vector masks(num_masks, 0); + for (int64_t i = 0; i < num_boxes; ++i) { + if (masks[i / 64] & 1ULL << (i % 64)) + continue; + T box_1[5]; + for (int k = 0; k < 5; ++k) { + box_1[k] = boxes_data[i * 5 + k]; + } + for (int64_t j = i + 1; j < num_boxes; ++j) { + if (masks[j / 64] & 1ULL << (j % 64)) + continue; + T box_2[5]; + for (int k = 0; k < 5; ++k) { + box_2[k] = boxes_data[j * 5 + k]; + } + if (rbox_iou_single(box_1, box_2) > threshold) { + masks[j / 64] |= 1ULL << (j % 64); + } + } + } + int64_t output_data_idx = 0; + for (int64_t i = 0; i < num_boxes; ++i) { + if (masks[i / 64] & 1ULL << (i % 64)) + continue; + output_data[output_data_idx++] = i; + } + *num_keep_boxes = output_data_idx; + for (; output_data_idx < num_boxes; ++output_data_idx) { + output_data[output_data_idx] = 0; + } +} + +#define CHECK_INPUT_CPU(x) \ + PD_CHECK(x.is_cpu(), #x " must be a CPU Tensor.") + +std::vector NMSRotatedCPUForward(const paddle::Tensor &boxes, + const paddle::Tensor &scores, + float threshold) { + CHECK_INPUT_CPU(boxes); + CHECK_INPUT_CPU(scores); + + auto num_boxes = boxes.shape()[0]; + + auto order_t = + std::get<1>(paddle::argsort(scores, /* axis=*/0, /* descending=*/true)); + auto boxes_sorted = paddle::gather(boxes, order_t, /* axis=*/0); + + auto keep = + paddle::empty({num_boxes}, paddle::DataType::INT64, paddle::CPUPlace()); + int64_t num_keep_boxes = 0; + + PD_DISPATCH_FLOATING_TYPES(boxes.type(), "nms_rotated_cpu_kernel", ([&] { + nms_rotated_cpu_kernel( + boxes_sorted.data(), threshold, + num_boxes, &num_keep_boxes, + keep.data()); + })); + + keep = keep.slice(0, num_keep_boxes); + return {paddle::gather(order_t, keep, /* axis=*/0)}; +} + +#ifdef PADDLE_WITH_CUDA +std::vector NMSRotatedCUDAForward(const paddle::Tensor &boxes, + const paddle::Tensor &scores, + float threshold); +#endif + +std::vector NMSRotatedForward(const paddle::Tensor &boxes, + const paddle::Tensor &scores, + float threshold) { + if (boxes.is_cpu()) { + return NMSRotatedCPUForward(boxes, scores, threshold); +#ifdef PADDLE_WITH_CUDA + } else if (boxes.is_gpu()) { + return NMSRotatedCUDAForward(boxes, scores, threshold); +#endif + } +} + +std::vector> +NMSRotatedInferShape(std::vector boxes_shape, + std::vector scores_shape) { + return {{-1}}; +} + +std::vector NMSRotatedInferDtype(paddle::DataType t1, + paddle::DataType t2) { + return {paddle::DataType::INT64}; +} + +PD_BUILD_OP(nms_rotated) + .Inputs({"Boxes", "Scores"}) + .Outputs({"Output"}) + .Attrs({"threshold: float"}) + .SetKernelFn(PD_KERNEL(NMSRotatedForward)) + .SetInferShapeFn(PD_INFER_SHAPE(NMSRotatedInferShape)) + .SetInferDtypeFn(PD_INFER_DTYPE(NMSRotatedInferDtype)); \ No newline at end of file diff --git a/ppdet/ext_op/csrc/nms_rotated/nms_rotated.cu b/ppdet/ext_op/csrc/nms_rotated/nms_rotated.cu new file mode 100644 index 000000000..d20dddb57 --- /dev/null +++ b/ppdet/ext_op/csrc/nms_rotated/nms_rotated.cu @@ -0,0 +1,96 @@ +// Copyright (c) 2022 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 "../rbox_iou/rbox_iou_utils.h" +#include "paddle/extension.h" + +static const int64_t threadsPerBlock = sizeof(int64_t) * 8; + +template +__global__ void +nms_rotated_cuda_kernel(const T *boxes_data, const float threshold, + const int64_t num_boxes, int64_t *masks) { + auto raw_start = blockIdx.y; + auto col_start = blockIdx.x; + if (raw_start > col_start) + return; + const int raw_last_storage = + min(num_boxes - raw_start * threadsPerBlock, threadsPerBlock); + const int col_last_storage = + min(num_boxes - col_start * threadsPerBlock, threadsPerBlock); + if (threadIdx.x < raw_last_storage) { + int64_t mask = 0; + auto current_box_idx = raw_start * threadsPerBlock + threadIdx.x; + const T *current_box = boxes_data + current_box_idx * 5; + for (int i = 0; i < col_last_storage; ++i) { + const T *target_box = boxes_data + (col_start * threadsPerBlock + i) * 5; + if (rbox_iou_single(current_box, target_box) > threshold) { + mask |= 1ULL << i; + } + } + const int blocks_per_line = CeilDiv(num_boxes, threadsPerBlock); + masks[current_box_idx * blocks_per_line + col_start] = mask; + } +} + +#define CHECK_INPUT_GPU(x) \ + PD_CHECK(x.is_gpu(), #x " must be a GPU Tensor.") + +std::vector NMSRotatedCUDAForward(const paddle::Tensor &boxes, + const paddle::Tensor &scores, + float threshold) { + CHECK_INPUT_GPU(boxes); + CHECK_INPUT_GPU(scores); + + auto num_boxes = boxes.shape()[0]; + auto order_t = + std::get<1>(paddle::argsort(scores, /* axis=*/0, /* descending=*/true)); + auto boxes_sorted = paddle::gather(boxes, order_t, /* axis=*/0); + + const auto blocks_per_line = CeilDiv(num_boxes, threadsPerBlock); + dim3 block(threadsPerBlock); + dim3 grid(blocks_per_line, blocks_per_line); + auto mask_dev = paddle::empty({num_boxes * blocks_per_line}, + paddle::DataType::INT64, paddle::GPUPlace()); + + PD_DISPATCH_FLOATING_TYPES( + boxes.type(), "nms_rotated_cuda_kernel", ([&] { + nms_rotated_cuda_kernel<<>>( + boxes_sorted.data(), threshold, num_boxes, + mask_dev.data()); + })); + + auto mask_host = mask_dev.copy_to(paddle::CPUPlace(), true); + auto keep_host = + paddle::empty({num_boxes}, paddle::DataType::INT64, paddle::CPUPlace()); + int64_t *keep_host_ptr = keep_host.data(); + int64_t *mask_host_ptr = mask_host.data(); + std::vector remv(blocks_per_line); + int64_t last_box_num = 0; + for (int64_t i = 0; i < num_boxes; ++i) { + auto remv_element_id = i / threadsPerBlock; + auto remv_bit_id = i % threadsPerBlock; + if (!(remv[remv_element_id] & 1ULL << remv_bit_id)) { + keep_host_ptr[last_box_num++] = i; + int64_t *current_mask = mask_host_ptr + i * blocks_per_line; + for (auto j = remv_element_id; j < blocks_per_line; ++j) { + remv[j] |= current_mask[j]; + } + } + } + + keep_host = keep_host.slice(0, last_box_num); + auto keep_dev = keep_host.copy_to(paddle::GPUPlace(), true); + return {paddle::gather(order_t, keep_dev, /* axis=*/0)}; +} \ No newline at end of file diff --git a/ppdet/ext_op/csrc/rbox_iou/rbox_iou.cc b/ppdet/ext_op/csrc/rbox_iou/rbox_iou.cc new file mode 100644 index 000000000..c8e7528d3 --- /dev/null +++ b/ppdet/ext_op/csrc/rbox_iou/rbox_iou.cc @@ -0,0 +1,95 @@ +// Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// 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. +// +// The code is based on +// https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/csrc/box_iou_rotated/ + +#include "paddle/extension.h" +#include "rbox_iou_utils.h" + +template +void rbox_iou_cpu_kernel(const int rbox1_num, const int rbox2_num, + const T *rbox1_data_ptr, const T *rbox2_data_ptr, + T *output_data_ptr) { + + int i, j; + for (i = 0; i < rbox1_num; i++) { + for (j = 0; j < rbox2_num; j++) { + int offset = i * rbox2_num + j; + output_data_ptr[offset] = + rbox_iou_single(rbox1_data_ptr + i * 5, rbox2_data_ptr + j * 5); + } + } +} + +#define CHECK_INPUT_CPU(x) \ + PD_CHECK(x.is_cpu(), #x " must be a CPU Tensor.") + +std::vector RboxIouCPUForward(const paddle::Tensor &rbox1, + const paddle::Tensor &rbox2) { + CHECK_INPUT_CPU(rbox1); + CHECK_INPUT_CPU(rbox2); + + auto rbox1_num = rbox1.shape()[0]; + auto rbox2_num = rbox2.shape()[0]; + + auto output = + paddle::empty({rbox1_num, rbox2_num}, rbox1.dtype(), paddle::CPUPlace()); + + PD_DISPATCH_FLOATING_TYPES(rbox1.type(), "rbox_iou_cpu_kernel", ([&] { + rbox_iou_cpu_kernel( + rbox1_num, rbox2_num, rbox1.data(), + rbox2.data(), output.data()); + })); + + return {output}; +} + +#ifdef PADDLE_WITH_CUDA +std::vector RboxIouCUDAForward(const paddle::Tensor &rbox1, + const paddle::Tensor &rbox2); +#endif + +#define CHECK_INPUT_SAME(x1, x2) \ + PD_CHECK(x1.place() == x2.place(), "input must be smae pacle.") + +std::vector RboxIouForward(const paddle::Tensor &rbox1, + const paddle::Tensor &rbox2) { + CHECK_INPUT_SAME(rbox1, rbox2); + if (rbox1.is_cpu()) { + return RboxIouCPUForward(rbox1, rbox2); +#ifdef PADDLE_WITH_CUDA + } else if (rbox1.is_gpu()) { + return RboxIouCUDAForward(rbox1, rbox2); +#endif + } +} + +std::vector> +RboxIouInferShape(std::vector rbox1_shape, + std::vector rbox2_shape) { + return {{rbox1_shape[0], rbox2_shape[0]}}; +} + +std::vector RboxIouInferDtype(paddle::DataType t1, + paddle::DataType t2) { + return {t1}; +} + +PD_BUILD_OP(rbox_iou) + .Inputs({"RBox1", "RBox2"}) + .Outputs({"Output"}) + .SetKernelFn(PD_KERNEL(RboxIouForward)) + .SetInferShapeFn(PD_INFER_SHAPE(RboxIouInferShape)) + .SetInferDtypeFn(PD_INFER_DTYPE(RboxIouInferDtype)); diff --git a/ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.cu b/ppdet/ext_op/csrc/rbox_iou/rbox_iou.cu similarity index 90% rename from ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.cu rename to ppdet/ext_op/csrc/rbox_iou/rbox_iou.cu index 16d1d36f1..baedb6ded 100644 --- a/ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.cu +++ b/ppdet/ext_op/csrc/rbox_iou/rbox_iou.cu @@ -13,21 +13,15 @@ // limitations under the License. // // The code is based on -// https://github.com/csuhan/s2anet/blob/master/mmdet/ops/box_iou_rotated +// https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/csrc/box_iou_rotated/ #include "paddle/extension.h" -#include "rbox_iou_op.h" +#include "rbox_iou_utils.h" // 2D block with 32 * 16 = 512 threads per block const int BLOCK_DIM_X = 32; const int BLOCK_DIM_Y = 16; -/** - Computes ceil(a / b) -*/ - -static inline int CeilDiv(const int a, const int b) { return (a + b - 1) / b; } - template __global__ void rbox_iou_cuda_kernel(const int rbox1_num, const int rbox2_num, const T *rbox1_data_ptr, @@ -85,7 +79,7 @@ __global__ void rbox_iou_cuda_kernel(const int rbox1_num, const int rbox2_num, } #define CHECK_INPUT_GPU(x) \ - PD_CHECK(x.place() == paddle::PlaceType::kGPU, #x " must be a GPU Tensor.") + PD_CHECK(x.is_gpu(), #x " must be a GPU Tensor.") std::vector RboxIouCUDAForward(const paddle::Tensor &rbox1, const paddle::Tensor &rbox2) { @@ -95,7 +89,8 @@ std::vector RboxIouCUDAForward(const paddle::Tensor &rbox1, auto rbox1_num = rbox1.shape()[0]; auto rbox2_num = rbox2.shape()[0]; - auto output = paddle::Tensor(paddle::PlaceType::kGPU, {rbox1_num, rbox2_num}); + auto output = + paddle::empty({rbox1_num, rbox2_num}, rbox1.dtype(), paddle::GPUPlace()); const int blocks_x = CeilDiv(rbox1_num, BLOCK_DIM_X); const int blocks_y = CeilDiv(rbox2_num, BLOCK_DIM_Y); @@ -107,7 +102,7 @@ std::vector RboxIouCUDAForward(const paddle::Tensor &rbox1, rbox1.type(), "rbox_iou_cuda_kernel", ([&] { rbox_iou_cuda_kernel<<>>( rbox1_num, rbox2_num, rbox1.data(), rbox2.data(), - output.mutable_data()); + output.data()); })); return {output}; diff --git a/ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.cc b/ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.cc deleted file mode 100644 index 6031953d2..000000000 --- a/ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.cc +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// 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. -// -// The code is based on https://github.com/csuhan/s2anet/blob/master/mmdet/ops/box_iou_rotated - -#include "rbox_iou_op.h" -#include "paddle/extension.h" - - -template -void rbox_iou_cpu_kernel( - const int rbox1_num, - const int rbox2_num, - const T* rbox1_data_ptr, - const T* rbox2_data_ptr, - T* output_data_ptr) { - - int i, j; - for (i = 0; i < rbox1_num; i++) { - for (j = 0; j < rbox2_num; j++) { - int offset = i * rbox2_num + j; - output_data_ptr[offset] = rbox_iou_single(rbox1_data_ptr + i * 5, rbox2_data_ptr + j * 5); - } - } -} - - -#define CHECK_INPUT_CPU(x) PD_CHECK(x.place() == paddle::PlaceType::kCPU, #x " must be a CPU Tensor.") - -std::vector RboxIouCPUForward(const paddle::Tensor& rbox1, const paddle::Tensor& rbox2) { - CHECK_INPUT_CPU(rbox1); - CHECK_INPUT_CPU(rbox2); - - auto rbox1_num = rbox1.shape()[0]; - auto rbox2_num = rbox2.shape()[0]; - - auto output = paddle::Tensor(paddle::PlaceType::kCPU, {rbox1_num, rbox2_num}); - - PD_DISPATCH_FLOATING_TYPES( - rbox1.type(), - "rbox_iou_cpu_kernel", - ([&] { - rbox_iou_cpu_kernel( - rbox1_num, - rbox2_num, - rbox1.data(), - rbox2.data(), - output.mutable_data()); - })); - - return {output}; -} - - -#ifdef PADDLE_WITH_CUDA -std::vector RboxIouCUDAForward(const paddle::Tensor& rbox1, const paddle::Tensor& rbox2); -#endif - - -#define CHECK_INPUT_SAME(x1, x2) PD_CHECK(x1.place() == x2.place(), "input must be smae pacle.") - -std::vector RboxIouForward(const paddle::Tensor& rbox1, const paddle::Tensor& rbox2) { - CHECK_INPUT_SAME(rbox1, rbox2); - if (rbox1.place() == paddle::PlaceType::kCPU) { - return RboxIouCPUForward(rbox1, rbox2); -#ifdef PADDLE_WITH_CUDA - } else if (rbox1.place() == paddle::PlaceType::kGPU) { - return RboxIouCUDAForward(rbox1, rbox2); -#endif - } -} - -std::vector> InferShape(std::vector rbox1_shape, std::vector rbox2_shape) { - return {{rbox1_shape[0], rbox2_shape[0]}}; -} - -std::vector InferDtype(paddle::DataType t1, paddle::DataType t2) { - return {t1}; -} - -PD_BUILD_OP(rbox_iou) - .Inputs({"RBOX1", "RBOX2"}) - .Outputs({"Output"}) - .SetKernelFn(PD_KERNEL(RboxIouForward)) - .SetInferShapeFn(PD_INFER_SHAPE(InferShape)) - .SetInferDtypeFn(PD_INFER_DTYPE(InferDtype)); diff --git a/ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.h b/ppdet/ext_op/csrc/rbox_iou/rbox_iou_utils.h similarity index 97% rename from ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.h rename to ppdet/ext_op/csrc/rbox_iou/rbox_iou_utils.h index fce66dea0..6f275dd65 100644 --- a/ppdet/ext_op/csrc/rbox_iou/rbox_iou_op.h +++ b/ppdet/ext_op/csrc/rbox_iou/rbox_iou_utils.h @@ -13,7 +13,7 @@ // limitations under the License. // // The code is based on -// https://github.com/csuhan/s2anet/blob/master/mmdet/ops/box_iou_rotated +// https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/csrc/box_iou_rotated/ #pragma once @@ -336,13 +336,21 @@ HOST_DEVICE_INLINE T rbox_iou_single(T const *const box1_raw, box2.h = box2_raw[3]; box2.a = box2_raw[4]; - const T area1 = box1.w * box1.h; - const T area2 = box2.w * box2.h; - if (area1 < 1e-14 || area2 < 1e-14) { + if (box1.w < 1e-2 || box1.h < 1e-2 || box2.w < 1e-2 || box2.h < 1e-2) { return 0.f; } + const T area1 = box1.w * box1.h; + const T area2 = box2.w * box2.h; const T intersection = rboxes_intersection(box1, box2); const T iou = intersection / (area1 + area2 - intersection); return iou; } + +/** + Computes ceil(a / b) +*/ + +HOST_DEVICE inline int CeilDiv(const int a, const int b) { + return (a + b - 1) / b; +} \ No newline at end of file diff --git a/ppdet/modeling/assigners/__init__.py b/ppdet/modeling/assigners/__init__.py index ded98c943..b344f0417 100644 --- a/ppdet/modeling/assigners/__init__.py +++ b/ppdet/modeling/assigners/__init__.py @@ -18,6 +18,7 @@ from . import atss_assigner from . import simota_assigner from . import max_iou_assigner from . import fcosr_assigner +from . import rotated_task_aligned_assigner from .utils import * from .task_aligned_assigner import * @@ -25,3 +26,4 @@ from .atss_assigner import * from .simota_assigner import * from .max_iou_assigner import * from .fcosr_assigner import * +from .rotated_task_aligned_assigner import * diff --git a/ppdet/modeling/assigners/fcosr_assigner.py b/ppdet/modeling/assigners/fcosr_assigner.py index 84f991023..46b743e60 100644 --- a/ppdet/modeling/assigners/fcosr_assigner.py +++ b/ppdet/modeling/assigners/fcosr_assigner.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ppdet/modeling/assigners/rotated_task_aligned_assigner.py b/ppdet/modeling/assigners/rotated_task_aligned_assigner.py new file mode 100644 index 000000000..eeb9a68b6 --- /dev/null +++ b/ppdet/modeling/assigners/rotated_task_aligned_assigner.py @@ -0,0 +1,164 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# 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 paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from ppdet.core.workspace import register +from ..rbox_utils import rotated_iou_similarity, check_points_in_rotated_boxes +from .utils import gather_topk_anchors, compute_max_iou_anchor + +__all__ = ['RotatedTaskAlignedAssigner'] + + +@register +class RotatedTaskAlignedAssigner(nn.Layer): + """TOOD: Task-aligned One-stage Object Detection + """ + + def __init__(self, topk=13, alpha=1.0, beta=6.0, eps=1e-9): + super(RotatedTaskAlignedAssigner, self).__init__() + self.topk = topk + self.alpha = alpha + self.beta = beta + self.eps = eps + + @paddle.no_grad() + def forward(self, + pred_scores, + pred_bboxes, + anchor_points, + num_anchors_list, + gt_labels, + gt_bboxes, + pad_gt_mask, + bg_index, + gt_scores=None): + r"""This code is based on + https://github.com/fcjian/TOOD/blob/master/mmdet/core/bbox/assigners/task_aligned_assigner.py + + The assignment is done in following steps + 1. compute alignment metric between all bbox (bbox of all pyramid levels) and gt + 2. select top-k bbox as candidates for each gt + 3. limit the positive sample's center in gt (because the anchor-free detector + only can predict positive distance) + 4. if an anchor box is assigned to multiple gts, the one with the + highest iou will be selected. + Args: + pred_scores (Tensor, float32): predicted class probability, shape(B, L, C) + pred_bboxes (Tensor, float32): predicted bounding boxes, shape(B, L, 5) + anchor_points (Tensor, float32): pre-defined anchors, shape(1, L, 2), "cxcy" format + num_anchors_list (List): num of anchors in each level, shape(L) + gt_labels (Tensor, int64|int32): Label of gt_bboxes, shape(B, n, 1) + gt_bboxes (Tensor, float32): Ground truth bboxes, shape(B, n, 5) + pad_gt_mask (Tensor, float32): 1 means bbox, 0 means no bbox, shape(B, n, 1) + bg_index (int): background index + gt_scores (Tensor|None, float32) Score of gt_bboxes, shape(B, n, 1) + Returns: + assigned_labels (Tensor): (B, L) + assigned_bboxes (Tensor): (B, L, 5) + assigned_scores (Tensor): (B, L, C) + """ + assert pred_scores.ndim == pred_bboxes.ndim + assert gt_labels.ndim == gt_bboxes.ndim and \ + gt_bboxes.ndim == 3 + + batch_size, num_anchors, num_classes = pred_scores.shape + _, num_max_boxes, _ = gt_bboxes.shape + + # negative batch + if num_max_boxes == 0: + assigned_labels = paddle.full( + [batch_size, num_anchors], bg_index, dtype=gt_labels.dtype) + assigned_bboxes = paddle.zeros([batch_size, num_anchors, 5]) + assigned_scores = paddle.zeros( + [batch_size, num_anchors, num_classes]) + return assigned_labels, assigned_bboxes, assigned_scores + + # compute iou between gt and pred bbox, [B, n, L] + ious = rotated_iou_similarity(gt_bboxes, pred_bboxes) + ious = paddle.where(ious > 1 + self.eps, paddle.zeros_like(ious), ious) + ious.stop_gradient = True + # gather pred bboxes class score + pred_scores = pred_scores.transpose([0, 2, 1]) + batch_ind = paddle.arange( + end=batch_size, dtype=gt_labels.dtype).unsqueeze(-1) + gt_labels_ind = paddle.stack( + [batch_ind.tile([1, num_max_boxes]), gt_labels.squeeze(-1)], + axis=-1) + bbox_cls_scores = paddle.gather_nd(pred_scores, gt_labels_ind) + # compute alignment metrics, [B, n, L] + alignment_metrics = bbox_cls_scores.pow(self.alpha) * ious.pow( + self.beta) + + # check the positive sample's center in gt, [B, n, L] + is_in_gts = check_points_in_rotated_boxes(anchor_points, gt_bboxes) + + # select topk largest alignment metrics pred bbox as candidates + # for each gt, [B, n, L] + is_in_topk = gather_topk_anchors( + alignment_metrics * is_in_gts, self.topk, topk_mask=pad_gt_mask) + + # select positive sample, [B, n, L] + mask_positive = is_in_topk * is_in_gts * pad_gt_mask + + # if an anchor box is assigned to multiple gts, + # the one with the highest iou will be selected, [B, n, L] + mask_positive_sum = mask_positive.sum(axis=-2) + if mask_positive_sum.max() > 1: + mask_multiple_gts = (mask_positive_sum.unsqueeze(1) > 1).tile( + [1, num_max_boxes, 1]) + is_max_iou = compute_max_iou_anchor(ious) + mask_positive = paddle.where(mask_multiple_gts, is_max_iou, + mask_positive) + mask_positive_sum = mask_positive.sum(axis=-2) + assigned_gt_index = mask_positive.argmax(axis=-2) + + # assigned target + assigned_gt_index = assigned_gt_index + batch_ind * num_max_boxes + assigned_labels = paddle.gather( + gt_labels.flatten(), assigned_gt_index.flatten(), axis=0) + assigned_labels = assigned_labels.reshape([batch_size, num_anchors]) + assigned_labels = paddle.where( + mask_positive_sum > 0, assigned_labels, + paddle.full_like(assigned_labels, bg_index)) + + assigned_bboxes = paddle.gather( + gt_bboxes.reshape([-1, 5]), assigned_gt_index.flatten(), axis=0) + assigned_bboxes = assigned_bboxes.reshape([batch_size, num_anchors, 5]) + + assigned_scores = F.one_hot(assigned_labels, num_classes + 1) + ind = list(range(num_classes + 1)) + ind.remove(bg_index) + assigned_scores = paddle.index_select( + assigned_scores, paddle.to_tensor(ind), axis=-1) + # rescale alignment metrics + alignment_metrics *= mask_positive + max_metrics_per_instance = alignment_metrics.max(axis=-1, keepdim=True) + max_ious_per_instance = (ious * mask_positive).max(axis=-1, + keepdim=True) + alignment_metrics = alignment_metrics / ( + max_metrics_per_instance + self.eps) * max_ious_per_instance + alignment_metrics = alignment_metrics.max(-2).unsqueeze(-1) + assigned_scores = assigned_scores * alignment_metrics + + assigned_bboxes.stop_gradient = True + assigned_scores.stop_gradient = True + assigned_labels.stop_gradient = True + return assigned_labels, assigned_bboxes, assigned_scores diff --git a/ppdet/modeling/heads/__init__.py b/ppdet/modeling/heads/__init__.py index 85c6b47bf..1e7a6b97c 100644 --- a/ppdet/modeling/heads/__init__.py +++ b/ppdet/modeling/heads/__init__.py @@ -34,6 +34,7 @@ from . import tood_head from . import retina_head from . import ppyoloe_head from . import fcosr_head +from . import ppyoloe_r_head from . import ld_gfl_head from .bbox_head import * @@ -59,3 +60,4 @@ from .retina_head import * from .ppyoloe_head import * from .fcosr_head import * from .ld_gfl_head import * +from .ppyoloe_r_head import * diff --git a/ppdet/modeling/heads/fcosr_head.py b/ppdet/modeling/heads/fcosr_head.py index 06b84440e..97cd949d7 100644 --- a/ppdet/modeling/heads/fcosr_head.py +++ b/ppdet/modeling/heads/fcosr_head.py @@ -205,8 +205,8 @@ class FCOSRHead(nn.Layer): anchor_points = [] stride_tensor = [] num_anchors_list = [] - for i, stride in enumerate(self.fpn_strides): - _, _, h, w = feats[i].shape + for feat, stride in zip(feats, self.fpn_strides): + _, _, h, w = paddle.shape(feat) shift_x = (paddle.arange(end=w) + 0.5) * stride shift_y = (paddle.arange(end=h) + 0.5) * stride shift_y, shift_x = paddle.meshgrid(shift_y, shift_x) diff --git a/ppdet/modeling/heads/ppyoloe_r_head.py b/ppdet/modeling/heads/ppyoloe_r_head.py new file mode 100644 index 000000000..89cb0fa82 --- /dev/null +++ b/ppdet/modeling/heads/ppyoloe_r_head.py @@ -0,0 +1,419 @@ +# Copyright (c) 2022 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 paddle +import paddle.nn as nn +import paddle.nn.functional as F +from ppdet.core.workspace import register + +from ..losses import ProbIoULoss +from ..initializer import bias_init_with_prob, constant_, normal_, vector_ +from ppdet.modeling.backbones.cspresnet import ConvBNLayer +from ppdet.modeling.ops import get_static_shape, get_act_fn, anchor_generator +from ppdet.modeling.layers import MultiClassNMS + +__all__ = ['PPYOLOERHead'] + + +class ESEAttn(nn.Layer): + def __init__(self, feat_channels, act='swish'): + super(ESEAttn, self).__init__() + self.fc = nn.Conv2D(feat_channels, feat_channels, 1) + self.conv = ConvBNLayer(feat_channels, feat_channels, 1, act=act) + + self._init_weights() + + def _init_weights(self): + normal_(self.fc.weight, std=0.01) + + def forward(self, feat, avg_feat): + weight = F.sigmoid(self.fc(avg_feat)) + return self.conv(feat * weight) + + +@register +class PPYOLOERHead(nn.Layer): + __shared__ = ['num_classes', 'trt'] + __inject__ = ['static_assigner', 'assigner', 'nms'] + + def __init__(self, + in_channels=[1024, 512, 256], + num_classes=15, + act='swish', + fpn_strides=(32, 16, 8), + grid_cell_offset=0.5, + angle_max=90, + use_varifocal_loss=True, + static_assigner_epoch=4, + trt=False, + static_assigner='ATSSAssigner', + assigner='TaskAlignedAssigner', + nms='MultiClassNMS', + loss_weight={'class': 1.0, + 'iou': 2.5, + 'dfl': 0.05}): + super(PPYOLOERHead, self).__init__() + assert len(in_channels) > 0, "len(in_channels) should > 0" + self.in_channels = in_channels + self.num_classes = num_classes + self.fpn_strides = fpn_strides + self.grid_cell_offset = grid_cell_offset + self.angle_max = angle_max + self.loss_weight = loss_weight + self.use_varifocal_loss = use_varifocal_loss + self.half_pi = paddle.to_tensor( + [1.5707963267948966], dtype=paddle.float32) + self.half_pi_bin = self.half_pi / angle_max + self.iou_loss = ProbIoULoss() + self.static_assigner_epoch = static_assigner_epoch + self.static_assigner = static_assigner + self.assigner = assigner + self.nms = nms + # stem + self.stem_cls = nn.LayerList() + self.stem_reg = nn.LayerList() + self.stem_angle = nn.LayerList() + act = get_act_fn( + act, trt=trt) if act is None or isinstance(act, + (str, dict)) else act + self.trt = trt + for in_c in self.in_channels: + self.stem_cls.append(ESEAttn(in_c, act=act)) + self.stem_reg.append(ESEAttn(in_c, act=act)) + self.stem_angle.append(ESEAttn(in_c, act=act)) + # pred head + self.pred_cls = nn.LayerList() + self.pred_reg = nn.LayerList() + self.pred_angle = nn.LayerList() + for in_c in self.in_channels: + self.pred_cls.append( + nn.Conv2D( + in_c, self.num_classes, 3, padding=1)) + self.pred_reg.append(nn.Conv2D(in_c, 4, 3, padding=1)) + self.pred_angle.append( + nn.Conv2D( + in_c, self.angle_max + 1, 3, padding=1)) + self.angle_proj_conv = nn.Conv2D( + self.angle_max + 1, 1, 1, bias_attr=False) + self._init_weights() + + @classmethod + def from_config(cls, cfg, input_shape): + return {'in_channels': [i.channels for i in input_shape], } + + def _init_weights(self): + bias_cls = bias_init_with_prob(0.01) + bias_angle = [10.] + [1.] * self.angle_max + for cls_, reg_, angle_ in zip(self.pred_cls, self.pred_reg, + self.pred_angle): + normal_(cls_.weight, std=0.01) + constant_(cls_.bias, bias_cls) + normal_(reg_.weight, std=0.01) + constant_(reg_.bias) + constant_(angle_.weight) + vector_(angle_.bias, bias_angle) + + angle_proj = paddle.linspace(0, self.angle_max, self.angle_max + 1) + self.angle_proj = angle_proj * self.half_pi_bin + self.angle_proj_conv.weight.set_value( + self.angle_proj.reshape([1, self.angle_max + 1, 1, 1])) + self.angle_proj_conv.weight.stop_gradient = True + + def _generate_anchors(self, feats): + if self.trt: + anchor_points = [] + for feat, stride in zip(feats, self.fpn_strides): + _, _, h, w = paddle.shape(feat) + anchor, _ = anchor_generator( + feat, + stride * 4, + 1.0, [1.0, 1.0, 1.0, 1.0], [stride, stride], + offset=0.5) + x1, y1, x2, y2 = paddle.split(anchor, 4, axis=-1) + xc = (x1 + x2 + 1) / 2 + yc = (y1 + y2 + 1) / 2 + anchor_point = paddle.concat( + [xc, yc], axis=-1).reshape((1, h * w, 2)) + anchor_points.append(anchor_point) + anchor_points = paddle.concat(anchor_points, axis=1) + return anchor_points, None, None + else: + anchor_points = [] + stride_tensor = [] + num_anchors_list = [] + for feat, stride in zip(feats, self.fpn_strides): + _, _, h, w = paddle.shape(feat) + shift_x = (paddle.arange(end=w) + 0.5) * stride + shift_y = (paddle.arange(end=h) + 0.5) * stride + shift_y, shift_x = paddle.meshgrid(shift_y, shift_x) + anchor_point = paddle.cast( + paddle.stack( + [shift_x, shift_y], axis=-1), dtype='float32') + anchor_points.append(anchor_point.reshape([1, -1, 2])) + stride_tensor.append( + paddle.full( + [1, h * w, 1], stride, dtype='float32')) + num_anchors_list.append(h * w) + anchor_points = paddle.concat(anchor_points, axis=1) + stride_tensor = paddle.concat(stride_tensor, axis=1) + return anchor_points, stride_tensor, num_anchors_list + + def forward(self, feats, targets=None): + assert len(feats) == len(self.fpn_strides), \ + "The size of feats is not equal to size of fpn_strides" + + if self.training: + return self.forward_train(feats, targets) + else: + return self.forward_eval(feats) + + def forward_train(self, feats, targets): + anchor_points, stride_tensor, num_anchors_list = self._generate_anchors( + feats) + + cls_score_list, reg_dist_list, reg_angle_list = [], [], [] + for i, feat in enumerate(feats): + avg_feat = F.adaptive_avg_pool2d(feat, (1, 1)) + cls_logit = self.pred_cls[i](self.stem_cls[i](feat, avg_feat) + + feat) + reg_dist = self.pred_reg[i](self.stem_reg[i](feat, avg_feat)) + reg_angle = self.pred_angle[i](self.stem_angle[i](feat, avg_feat)) + # cls and reg + cls_score = F.sigmoid(cls_logit) + cls_score_list.append(cls_score.flatten(2).transpose([0, 2, 1])) + reg_dist_list.append(reg_dist.flatten(2).transpose([0, 2, 1])) + reg_angle_list.append(reg_angle.flatten(2).transpose([0, 2, 1])) + cls_score_list = paddle.concat(cls_score_list, axis=1) + reg_dist_list = paddle.concat(reg_dist_list, axis=1) + reg_angle_list = paddle.concat(reg_angle_list, axis=1) + + return self.get_loss([ + cls_score_list, reg_dist_list, reg_angle_list, anchor_points, + num_anchors_list, stride_tensor + ], targets) + + def forward_eval(self, feats): + cls_score_list, reg_box_list = [], [] + anchor_points, _, _ = self._generate_anchors(feats) + for i, (feat, stride) in enumerate(zip(feats, self.fpn_strides)): + b, _, h, w = paddle.shape(feat) + l = h * w + # cls + avg_feat = F.adaptive_avg_pool2d(feat, (1, 1)) + cls_logit = self.pred_cls[i](self.stem_cls[i](feat, avg_feat) + + feat) + # reg + reg_dist = self.pred_reg[i](self.stem_reg[i](feat, avg_feat)) + reg_xy, reg_wh = paddle.split(reg_dist, 2, axis=1) + reg_xy = reg_xy * stride + reg_wh = (F.elu(reg_wh) + 1.) * stride + reg_angle = self.pred_angle[i](self.stem_angle[i](feat, avg_feat)) + reg_angle = self.angle_proj_conv(F.softmax(reg_angle, axis=1)) + reg_box = paddle.concat([reg_xy, reg_wh, reg_angle], axis=1) + # cls and reg + cls_score = F.sigmoid(cls_logit) + cls_score_list.append(cls_score.reshape([b, self.num_classes, l])) + reg_box_list.append(reg_box.reshape([b, 5, l])) + + cls_score_list = paddle.concat(cls_score_list, axis=-1) + reg_box_list = paddle.concat(reg_box_list, axis=-1).transpose([0, 2, 1]) + reg_xy, reg_wha = paddle.split(reg_box_list, [2, 3], axis=-1) + reg_xy = reg_xy + anchor_points + reg_box_list = paddle.concat([reg_xy, reg_wha], axis=-1) + return cls_score_list, reg_box_list + + def _bbox_decode(self, points, pred_dist, pred_angle, stride_tensor): + # predict vector to x, y, w, h, angle + b, l = pred_angle.shape[:2] + xy, wh = paddle.split(pred_dist, 2, axis=-1) + xy = xy * stride_tensor + points + wh = (F.elu(wh) + 1.) * stride_tensor + angle = F.softmax(pred_angle.reshape([b, l, 1, self.angle_max + 1 + ])).matmul(self.angle_proj) + return paddle.concat([xy, wh, angle], axis=-1) + + def get_loss(self, head_outs, gt_meta): + pred_scores, pred_dist, pred_angle, \ + anchor_points, num_anchors_list, stride_tensor = head_outs + # [B, N, 5] -> [B, N, 5] + pred_bboxes = self._bbox_decode(anchor_points, pred_dist, pred_angle, + stride_tensor) + gt_labels = gt_meta['gt_class'] + # [B, N, 5] + gt_bboxes = gt_meta['gt_rbox'] + pad_gt_mask = gt_meta['pad_gt_mask'] + # label assignment + if gt_meta['epoch_id'] < self.static_assigner_epoch: + assigned_labels, assigned_bboxes, assigned_scores = \ + self.static_assigner( + anchor_points, + stride_tensor, + num_anchors_list, + gt_labels, + gt_meta['gt_bbox'], + gt_bboxes, + pad_gt_mask, + self.num_classes, + pred_bboxes.detach() + ) + else: + assigned_labels, assigned_bboxes, assigned_scores = \ + self.assigner( + pred_scores.detach(), + pred_bboxes.detach(), + anchor_points, + num_anchors_list, + gt_labels, + gt_bboxes, + pad_gt_mask, + bg_index=self.num_classes) + alpha_l = -1 + # cls loss + if self.use_varifocal_loss: + one_hot_label = F.one_hot(assigned_labels, + self.num_classes + 1)[..., :-1] + loss_cls = self._varifocal_loss(pred_scores, assigned_scores, + one_hot_label) + else: + loss_cls = self._focal_loss(pred_scores, assigned_scores, alpha_l) + + assigned_scores_sum = assigned_scores.sum() + if paddle.distributed.get_world_size() > 1: + paddle.distributed.all_reduce(assigned_scores_sum) + assigned_scores_sum = paddle.clip( + assigned_scores_sum / paddle.distributed.get_world_size(), + min=1.) + else: + assigned_scores_sum = paddle.clip(assigned_scores_sum, min=1.) + loss_cls /= assigned_scores_sum + + loss_iou, loss_dfl = self._bbox_loss(pred_angle, pred_bboxes, + anchor_points, assigned_labels, + assigned_bboxes, assigned_scores, + assigned_scores_sum, stride_tensor) + + loss = self.loss_weight['class'] * loss_cls + \ + self.loss_weight['iou'] * loss_iou + \ + self.loss_weight['dfl'] * loss_dfl + out_dict = { + 'loss': loss, + 'loss_cls': loss_cls, + 'loss_iou': loss_iou, + 'loss_dfl': loss_dfl + } + return out_dict + + @staticmethod + def _focal_loss(score, label, alpha=0.25, gamma=2.0): + weight = (score - label).pow(gamma) + if alpha > 0: + alpha_t = alpha * label + (1 - alpha) * (1 - label) + weight *= alpha_t + loss = F.binary_cross_entropy( + score, label, weight=weight, reduction='sum') + return loss + + @staticmethod + def _varifocal_loss(pred_score, gt_score, label, alpha=0.75, gamma=2.0): + weight = alpha * pred_score.pow(gamma) * (1 - label) + gt_score * label + loss = F.binary_cross_entropy( + pred_score, gt_score, weight=weight, reduction='sum') + return loss + + @staticmethod + def _df_loss(pred_dist, target): + target_left = paddle.cast(target, 'int64') + target_right = target_left + 1 + weight_left = target_right.astype('float32') - target + weight_right = 1 - weight_left + loss_left = F.cross_entropy( + pred_dist, target_left, reduction='none') * weight_left + loss_right = F.cross_entropy( + pred_dist, target_right, reduction='none') * weight_right + return (loss_left + loss_right).mean(-1, keepdim=True) + + def _bbox_loss(self, pred_angle, pred_bboxes, anchor_points, + assigned_labels, assigned_bboxes, assigned_scores, + assigned_scores_sum, stride_tensor): + # select positive samples mask + mask_positive = (assigned_labels != self.num_classes) + num_pos = mask_positive.sum() + # pos/neg loss + if num_pos > 0: + # iou + bbox_mask = mask_positive.unsqueeze(-1).tile([1, 1, 5]) + pred_bboxes_pos = paddle.masked_select(pred_bboxes, + bbox_mask).reshape([-1, 5]) + assigned_bboxes_pos = paddle.masked_select( + assigned_bboxes, bbox_mask).reshape([-1, 5]) + bbox_weight = paddle.masked_select( + assigned_scores.sum(-1), mask_positive).reshape([-1]) + + loss_iou = self.iou_loss(pred_bboxes_pos, + assigned_bboxes_pos) * bbox_weight + loss_iou = loss_iou.sum() / assigned_scores_sum + + # dfl + angle_mask = mask_positive.unsqueeze(-1).tile( + [1, 1, self.angle_max + 1]) + pred_angle_pos = paddle.masked_select( + pred_angle, angle_mask).reshape([-1, self.angle_max + 1]) + assigned_angle_pos = ( + assigned_bboxes_pos[:, 4] / + self.half_pi_bin).clip(0, self.angle_max - 0.01) + loss_dfl = self._df_loss(pred_angle_pos, assigned_angle_pos) + else: + loss_iou = pred_bboxes.sum() * 0. + loss_dfl = paddle.zeros([1]) + + return loss_iou, loss_dfl + + def _box2corners(self, pred_bboxes): + """ convert (x, y, w, h, angle) to (x1, y1, x2, y2, x3, y3, x4, y4) + + Args: + pred_bboxes (Tensor): [B, N, 5] + + Returns: + polys (Tensor): [B, N, 8] + """ + x, y, w, h, angle = paddle.split(pred_bboxes, 5, axis=-1) + cos_a_half = paddle.cos(angle) * 0.5 + sin_a_half = paddle.sin(angle) * 0.5 + w_x = cos_a_half * w + w_y = sin_a_half * w + h_x = -sin_a_half * h + h_y = cos_a_half * h + return paddle.concat( + [ + x + w_x + h_x, y + w_y + h_y, x - w_x + h_x, y - w_y + h_y, + x - w_x - h_x, y - w_y - h_y, x + w_x - h_x, y + w_y - h_y + ], + axis=-1) + + def post_process(self, head_outs, scale_factor): + pred_scores, pred_bboxes = head_outs + # [B, N, 5] -> [B, N, 8] + pred_bboxes = self._box2corners(pred_bboxes) + # scale bbox to origin + scale_y, scale_x = paddle.split(scale_factor, 2, axis=-1) + scale_factor = paddle.concat( + [ + scale_x, scale_y, scale_x, scale_y, scale_x, scale_y, scale_x, + scale_y + ], + axis=-1).reshape([-1, 1, 8]) + pred_bboxes /= scale_factor + bbox_pred, bbox_num, _ = self.nms(pred_bboxes, pred_scores) + return bbox_pred, bbox_num diff --git a/ppdet/modeling/initializer.py b/ppdet/modeling/initializer.py index b482f133d..758eed240 100644 --- a/ppdet/modeling/initializer.py +++ b/ppdet/modeling/initializer.py @@ -118,6 +118,12 @@ def zeros_(tensor): return _no_grad_fill_(tensor, 0) +def vector_(tensor, vector): + with paddle.no_grad(): + tensor.set_value(paddle.to_tensor(vector, dtype=tensor.dtype)) + return tensor + + def _calculate_fan_in_and_fan_out(tensor, reverse=False): """ Calculate (fan_in, _fan_out) for tensor diff --git a/ppdet/modeling/necks/custom_pan.py b/ppdet/modeling/necks/custom_pan.py index 08de226de..bb7123c66 100644 --- a/ppdet/modeling/necks/custom_pan.py +++ b/ppdet/modeling/necks/custom_pan.py @@ -61,7 +61,14 @@ class SPP(nn.Layer): class CSPStage(nn.Layer): - def __init__(self, block_fn, ch_in, ch_out, n, act='swish', spp=False): + def __init__(self, + block_fn, + ch_in, + ch_out, + n, + act='swish', + spp=False, + use_alpha=False): super(CSPStage, self).__init__() ch_mid = int(ch_out // 2) @@ -72,7 +79,11 @@ class CSPStage(nn.Layer): for i in range(n): self.convs.add_sublayer( str(i), - eval(block_fn)(next_ch_in, ch_mid, act=act, shortcut=False)) + eval(block_fn)(next_ch_in, + ch_mid, + act=act, + shortcut=False, + use_alpha=use_alpha)) if i == (n - 1) // 2 and spp: self.convs.add_sublayer( 'spp', SPP(ch_mid * 4, ch_mid, 1, [5, 9, 13], act=act)) @@ -109,6 +120,7 @@ class CustomCSPPAN(nn.Layer): data_format='NCHW', width_mult=1.0, depth_mult=1.0, + use_alpha=False, trt=False): super(CustomCSPPAN, self).__init__() @@ -136,7 +148,8 @@ class CustomCSPPAN(nn.Layer): ch_out, block_num, act=act, - spp=(spp and i == 0))) + spp=(spp and i == 0), + use_alpha=use_alpha)) if drop_block: stage.add_sublayer('drop', DropBlock(block_size, keep_prob)) @@ -181,7 +194,8 @@ class CustomCSPPAN(nn.Layer): ch_out, block_num, act=act, - spp=False)) + spp=False, + use_alpha=use_alpha)) if drop_block: stage.add_sublayer('drop', DropBlock(block_size, keep_prob)) diff --git a/ppdet/modeling/ops.py b/ppdet/modeling/ops.py index fb9d98cf0..d9a1192d7 100644 --- a/ppdet/modeling/ops.py +++ b/ppdet/modeling/ops.py @@ -26,18 +26,9 @@ from paddle import in_dynamic_mode from paddle.common_ops_import import Variable, LayerHelper, check_variable_and_dtype, check_type, check_dtype __all__ = [ - 'prior_box', - 'generate_proposals', - 'box_coder', - 'multiclass_nms', - 'distribute_fpn_proposals', - 'matrix_nms', - 'batch_norm', - 'mish', - 'silu', - 'swish', - 'identity', - 'anchor_generator' + 'prior_box', 'generate_proposals', 'box_coder', 'multiclass_nms', + 'distribute_fpn_proposals', 'matrix_nms', 'batch_norm', 'mish', 'silu', + 'swish', 'identity', 'anchor_generator' ] @@ -118,6 +109,7 @@ def batch_norm(ch, return norm_layer + @paddle.jit.not_to_static def anchor_generator(input, anchor_sizes=None, diff --git a/ppdet/modeling/rbox_utils.py b/ppdet/modeling/rbox_utils.py index bde5320cb..a5f19a294 100644 --- a/ppdet/modeling/rbox_utils.py +++ b/ppdet/modeling/rbox_utils.py @@ -239,3 +239,57 @@ def check_points_in_polys(points, polys): is_in_polys = (ap_dot_ab >= 0) & (ap_dot_ab <= norm_ab) & ( ap_dot_ad >= 0) & (ap_dot_ad <= norm_ad) return is_in_polys + + +def check_points_in_rotated_boxes(points, boxes): + """Check whether point is in rotated boxes + + Args: + points (tensor): (1, L, 2) anchor points + boxes (tensor): [B, N, 5] gt_bboxes + eps (float): default 1e-9 + + Returns: + is_in_box (tensor): (B, N, L) + + """ + # [B, N, 5] -> [B, N, 4, 2] + corners = box2corners(boxes) + # [1, L, 2] -> [1, 1, L, 2] + points = points.unsqueeze(0) + # [B, N, 4, 2] -> [B, N, 1, 2] + a, b, c, d = corners.split(4, axis=2) + ab = b - a + ad = d - a + # [B, N, L, 2] + ap = points - a + # [B, N, L] + norm_ab = paddle.sum(ab * ab, axis=-1) + # [B, N, L] + norm_ad = paddle.sum(ad * ad, axis=-1) + # [B, N, L] dot product + ap_dot_ab = paddle.sum(ap * ab, axis=-1) + # [B, N, L] dot product + ap_dot_ad = paddle.sum(ap * ad, axis=-1) + # [B, N, L] = |A|*|B|*cos(theta) + is_in_box = (ap_dot_ab >= 0) & (ap_dot_ab <= norm_ab) & (ap_dot_ad >= 0) & ( + ap_dot_ad <= norm_ad) + return is_in_box + + +def rotated_iou_similarity(box1, box2, eps=1e-9, func=''): + """Calculate iou of box1 and box2 + + Args: + box1 (Tensor): box with the shape [N, M1, 5] + box2 (Tensor): box with the shape [N, M2, 5] + + Return: + iou (Tensor): iou between box1 and box2 with the shape [N, M1, M2] + """ + from ext_op import rbox_iou + rotated_ious = [] + for b1, b2 in zip(box1, box2): + rotated_ious.append(rbox_iou(b1, b2)) + + return paddle.stack(rotated_ious, axis=0) -- GitLab