Welcome to MMRotate’s documentation!¶
学习基础知识 (待更新)¶
本章将向您介绍旋转目标检测的基本概念,以及旋转目标检测的框架 MMRotate,并提供了详细教程的链接。
什么是旋转目标检测¶
问题定义¶
受益于通用检测的蓬勃发展,目前绝大多数的旋转检测模型都是基于经典的通用检测器。随着检测任务的发展, 水平框在一些细分领域上已经无法满足研究人员的需求。通过重新定义目标表示形式以及增加回归自由度数量 的操作来实现旋转矩形框、四边形甚至任意形状检测,我们称之为旋转目标检测。如何更加高效地进行高精度 的旋转目标检测已成为当下的研究热点。下面列举一些旋转目标检测已经被应用或者有巨大潜力的领域: 人脸识别、场景文字、遥感影像、自动驾驶、医学图像、机器人抓取等。
什么是旋转框¶
旋转目标检测与通用目标检测最大的不同就是用旋转框标注来代替水平框标注,它们的定义如下:
水平框: 宽沿
x
轴方向,高沿y
轴方向的矩形。通常可以用2个对角顶点的坐标表示(x_i, y_i)
(i = 1, 2),也可以用中心点坐标以及宽和高表示(x_center, y_center, width, height)
。旋转框: 由水平框绕中心点旋转一个角度
angle
得到,通过添加一个弧度参数得到其旋转框定义法(x_center, y_center, width, height, theta)
。其中,theta = angle * pi / 180
, 单位为rad
。当旋转的角度为90°的倍数时,旋转框退化为水平框。标注软件导出的旋转框标注通常为多边形(xr_i, yr_i)
(i = 1, 2, 3, 4),在训练时需要转换为旋转框定义法。
注解
在 MMRotate 中,角度参数的单位均为弧度。
旋转方向¶
旋转框可以由水平框绕其中心点顺时针旋转或逆时针旋转得到。旋转方向和坐标系的选择密切相关。
图像空间采用右手坐标系 (y,x)
,其中 y 是上->下
,x 是左->右
。
此时存在2种相反的旋转方向:
顺时针 (
CW
)
CW
的示意图
0-------------------> x (0 rad)
| A-------------B
| | |
| | box h
| | angle=0 |
| D------w------C
v
y (pi/2 rad)
CW
的旋转矩阵
CW
的旋转变换
逆时针 (
CCW
)
CCW
的示意图
0-------------------> x (0 rad)
| A-------------B
| | |
| | box h
| | angle=0 |
| D------w------C
v
y (-pi/2 rad)
CCW
的旋转矩阵
CCW
的旋转变换
在MMCV中可以设置旋转方向的算子有:
box_iou_rotated (默认为
CW
)nms_rotated (默认为
CW
)RoIAlignRotated (默认为
CCW
)RiRoIAlignRotated (默认为
CCW
)。
注解
在MMRotate 中,旋转框的旋转方向均为 CW
。
旋转框定义法¶
由于 theta
定义范围的不同,在旋转目标检测中逐渐派生出如下3种旋转框定义法:
\(D_{oc^{\prime}}\) : OpenCV 定义法,
angle∈(0, 90°]
,theta∈(0, pi / 2]
,width
与x
正半轴之间的夹角为正的锐角。该定义法源于 OpenCV 中的cv2.minAreaRect
函数, 其返回值为(0, 90°]
。\(D_{le135}\) : 长边 135° 定义法,
angle∈[-45°, 135°)
,theta∈[-pi / 4, 3 * pi / 4)
并且width > height
。\(D_{le90}\) : 长边 90° 定义法,
angle∈[-90°, 90°)
,theta∈[-pi / 2, pi / 2)
并且width > height
。

三种定义法之间的转换关系在 MMRotate 内部并不涉及,因此不多做介绍。如果想了解更多的细节,可以参考这篇博客。
注解
MMRotate 同时支持上述三种旋转框定义法,可以通过配置文件灵活切换。
需要注意的是,在 4.5.1 之前的版本中,cv2.minAreaRect
的返回值为 [-90°, 0°)
(参考资料)。为了便于区分,
将老版本的 OpenCV 定义法记作 \(D_{oc}\)。
\(D_{oc^{\prime}}\): OpenCV 定义法,
opencv>=4.5.1
,angle∈(0, 90°]
,theta∈(0, pi / 2]
。\(D_{oc}\): 老版的 OpenCV 定义法,
opencv<4.5.1
,angle∈[-90°, 0°)
,theta∈[-pi / 2, 0)
。

两种 OpenCV 定义法的转换关系如下:
注解
不管您使用的 OpenCV 版本是多少, MMRotate 都会将 OpenCV 定义法的 theta 转换为 (0, pi / 2]。
评估¶
评估 mAP 的代码中涉及 IoU 的计算,可以直接计算旋转框 IoU,也可以将旋转框转换为多边形,然后 计算多边形 IoU (DOTA 在线评估使用的是计算多边形 IoU)。
什么是 MMRotate¶
MMRotate 是一个为旋转目标检测方法提供统一训练和评估框架的工具箱,以下是其整体框架::

MMRotate 包括四个部分, datasets
, models
, core
and apis
.
datasets
用于数据加载和数据增强。 在这部分,我们支持了各种旋转目标检测数据集和数据增强预处理。models
包括模型和损失函数。core
为模型训练和评估提供工具。apis
为模型训练、测试和推理提供高级 API。
MMRotate 的模块设计如下图所示:

其中由于旋转框定义法不同而需要注意的地方有如下几个:
读取标注
数据增强
指派样本
评估指标
开始你的第一步¶
需要准备的依赖环境¶
在本小节中将会演示如何准备一个Pytorch的环境。
MMRotate可以在Linux和Windows系统工作,需要以下环境版本:Python 3.7+, CUDA 9.2+ and PyTorch 1.6+。
注解
如果您对于Pytorch很熟悉并且已经完成了其安装步骤,您可以跳过本部分内容直接查阅安装的部分。当然,如果您没有准备好这部分的安装,请按照以下流程进行准备。
第一步: 从 Miniconda 下载并且安装Miniconda。
第二步: 创建一个虚拟环境并且切换至该虚拟环境中。
conda create --name openmmlab python=3.8 -y
conda activate openmmlab
第三步: 根据 Pytorch的官方说明 安装Pytorch, 例如:
conda install pytorch==1.8.0 torchvision==0.9.0 cudatoolkit=10.2 -c pytorch
安装¶
我们强烈建议用户使用以下方式安装MMRotate,这是最方便的。当然,还有多种自定义的安装方式可供经验丰富者选择,您可以查阅 自定义安装 来获取更多相关的帮助。
最佳的安装方式¶
第一步: 安装 MMEngine 和 MMCV ,并且我们建议使用 MIM 来完成安装。
pip install -U openmim
mim install mmengine
mim install "mmcv>=2.0.0rc2"
第二步: 安装 MMDetection 。
mim install 'mmdet>=3.0.0rc2'
注意:如果您需要进行部分代码的修改与开发,请从源码获取MMDetection并且进行以下示例的安装:
git clone https://github.com/open-mmlab/mmdetection.git -b dev-3.x
# "-b dev-3.x" means checkout to the `dev-3.x` branch.
cd mmdetection
pip install -v -e .
# "-v" means verbose, or more output
# "-e" means installing a project in editable mode,
# thus any local modifications made to the code will take effect without reinstallation.
第三步: 安装 MMRotate。
情形a:如果您需要对于MMRotate进行代码上的开发与修改,请从源码进行以下示例的安装:
git clone https://github.com/open-mmlab/mmrotate.git -b dev-1.x
# "-b dev-1.x" means checkout to the `dev-1.x` branch.
cd mmrotate
pip install -v -e .
# "-v" means verbose, or more output
# "-e" means installing a project in editable mode,
# thus any local modifications made to the code will take effect without reinstallation.
情形b:如果您只是使用MMRotate作为依赖项或者是第三方软件包,则直接使用pip进行以下示例的安装即可:
pip install mmrotate
验证安装是否正确¶
为了验证MMRotate是否正确地被安装到了您的环境中,我们提供了一些demo以供测试。
第一步: 为了进行测试的验证,我们需要下载config文件与checkpoint文件。
mim download mmrotate --config oriented-rcnn-le90_r50_fpn_1x_dota --dest .
下载过程可能会依据您的网络状况花费数秒或者更多的时间。当下载完成之后,您可以在您的当前目录下找到 oriented-rcnn-le90_r50_fpn_1x_dota.py
和 oriented_rcnn_r50_fpn_1x_dota_le90-6d2b2ce0.pth
两个文件。
第二步: 使用推理demo完成验证
情形 (a):如果您是通过源码方式安装的MMRotate,直接在命令行中运行以下代码即可:
python demo/image_demo.py demo/demo.jpg oriented-rcnn-le90_r50_fpn_1x_dota.py oriented_rcnn_r50_fpn_1x_dota_le90-6d2b2ce0.pth --out-file result.jpg
您将会在您的当前文件夹下看到一张 result.jpg
为文件名的图片,在该图片中完成了对于小轿车和公交车等物体的旋转锚定框的绘制。
情形 (b): 如果您是使用pip的方式安装的MMRotate,打开您的python控制台并使用代码:
from mmdet.apis import init_detector, inference_detector
import mmrotate
config_file = 'oriented-rcnn-le90_r50_fpn_1x_dota.py'
checkpoint_file = 'oriented_rcnn_r50_fpn_1x_dota_le90-6d2b2ce0.pth'
model = init_detector(config_file, checkpoint_file, device='cuda:0')
inference_detector(model, 'demo/demo.jpg')
您将会看到一系列数组列表被打印出来,这代表着被检测到的旋转锚定框。
自定义安装¶
CUDA 版本¶
安装Pytorch之前,您需要指定安装的CUDA版本。如果您不清楚您的CUDA版本的安装选择,我们建议:
对于基于安培架构的NVIDIA GPU,例如GeForce 30系列和NVIDIA A100,请使用CUDA 11。
对于其他的NVIDIA GPU,CUDA 11同样可以兼容,但是CUDA 10.2更加轻量并且更好被兼容。
请确保GPU的驱动程序满足最低的版本需求,您可以查阅 表格 以了解更多信息。
注解
如果您按照我们推荐的最佳的安装方式进行安装流程,安装CUDA的运行库就已经足够了,因为这不需要在本地进行CUDA代码的编译工作。但是如果您想从源码对于MMCV进行编译或者开发其他的CUDA算子,您需要从NVIDIA官网完整地安装CUDA工具包 官网 并且CUDA工具包的版本应该和您使用的Pytorch版本对应。例如您可以使用 ‘conda install’ 命令来安装指定版本的CUDA工具包。
不使用MIM安装MMCV¶
MMCV有一些C++和CUDA扩展的使用,因此MMCV与Pytorch有着复杂的依赖关系。MIM会自动处理这些依赖关系使整个安装过程更加简便,但是您也可以选择不使用MIM这个工具。
如果您想使用pip进行MMCV的安装,而不是MIM,请按照 MMCV安装指南 。您需要根据Pytorch版本和CUDA版本手动指定 find-url。
举个例子,以下代码示例是在PyTorch 1.9.x于CUDA 10.2的环境下进行MMCV的安装:
pip install mmcv -f https://download.openmmlab.com/mmcv/dist/cu102/torch1.8/index.html
在Google Colab上安装¶
Google Colab 通常已经完成了Pytorch的安装, 因此我们只需要按照以下步骤完成MMCV和MMDetection的安装即可。
第一步: 使用 MIM 安装 MMCV 和 MMDetection :
!pip3 install -U openmim
!mim install "mmcv>=2.0.0rc2"
!mim install 'mmdet>=3.0.0rc2'
第二步: 从源码安装MMRotate:
!git clone https://github.com/open-mmlab/mmrotate.git -b dev-1.x
%cd mmrotate
!pip install -e .
第三步: 验证安装是否正确
import mmrotate
print(mmrotate.__version__)
# Example output: 1.x
注解
在Jupyter中,感叹号 !
用于调用外部可执行文件,而符号 %cd
是一个用于更改Python当前工作目录的 魔术指令 。
使用Docker安装MMRotate¶
我们同样提供了 Dockerfile 用于创建镜像。请确认您的 docker version >=19.03。
# build an image with PyTorch 1.6, CUDA 10.1
# If you prefer other versions, just modified the Dockerfile
docker build -t mmrotate docker/
然后运行以下指令:
docker run --gpus all --shm-size=8g -it -v {DATA_DIR}:/mmrotate/data mmrotate
训练 & 测试¶
MMRotate 提供了几十个预训练模型 Model Zoo, 并且支持了多个标准数据集, 包括 DOTA, HRSC2016, SSDD, HRSID 等。本说明将展示如何在这些现有模型和标准数据集上执行常见任务:
学习配置文件 (待更新)¶
我们在配置文件中支持了继承和模块化,这便于进行各种实验。
如果需要检查配置文件,可以通过运行 python tools/misc/print_config.py /PATH/TO/CONFIG
来查看完整的配置。
mmrotate 是建立在 mmdet 之上的,
因此强烈建议学习 mmdet 的基本知识。
通过脚本参数修改配置¶
当运行 tools/train.py
或者 tools/test.py
时,可以通过 --cfg-options
来修改配置。
更新字典链的配置
可以按照原始配置文件中的 dict 键顺序地指定配置预选项。 例如,使用
--cfg-options model.backbone.norm_eval=False
将模型主干网络中的所有 BN 模块都改为train
模式。更新配置列表中的键
在配置文件里,一些字典型的配置被包含在列表中。例如,数据训练流程
data.train.pipeline
通常是一个列表,比如[dict(type='LoadImageFromFile'), ...]
。 如果需要将'LoadImageFromFile'
改成'LoadImageFromWebcam'
,需要写成下述形式:--cfg-options data.train.pipeline.0.type=LoadImageFromWebcam
。更新列表或元组的值
如果要更新的值是列表或元组。例如,配置文件通常设置
workflow=[('train', 1)]
,如果需要改变这个键,可以通过--cfg-options workflow="[(train,1),(val,1)]"
来重新设置。需要注意,引号 ” 是支持列表或元组数据类型所必需的,并且在指定值的引号内不允许有空格。
配置文件名称风格¶
我们遵循以下样式来命名配置文件。建议贡献者遵循相同的风格。
{model}_[model setting]_{backbone}_{neck}_[norm setting]_[misc]_[gpu x batch_per_gpu]_{dataset}_{data setting}_{angle version}
{xxx}
是被要求的文件 [yyy]
是可选的。
{model}
: 模型种类,例如rotated_faster_rcnn
,rotated_retinanet
等。[model setting]
: 特定的模型,例如hbb
forrotated_retinanet
等。{backbone}
: 主干网络种类例如r50
(ResNet-50),swin_tiny
(SWIN-tiny) 。{neck}
: Neck 模型的种类包括fpn
,refpn
。[norm_setting]
: 默认使用bn
(Batch Normalization),其他指定可以有gn
(Group Normalization),syncbn
(Synchronized Batch Normalization) 等。gn-head
/gn-neck
表示 GN 仅应用于网络的 Head 或 Neck,gn-all
表示 GN 用于整个模型, 例如主干网络、Neck 和 Head。[misc]
: 模型中各式各样的设置/插件,例如dconv
、gcb
、attention
、albu
、mstrain
等。[gpu x batch_per_gpu]
: GPU 数量和每个 GPU 的样本数,默认使用1xb2
。{dataset}
:数据集,例如dota
。{angle version}
:旋转定义方式,例如oc
,le135
或者le90
。
RotatedRetinaNet 配置文件示例¶
为了帮助用户对 MMRotate 检测系统中的完整配置和模块有一个基本的了解 我们对使用 ResNet50 和 FPN 的 RotatedRetinaNet 的配置文件进行简要注释说明。更详细的用法和各个模块对应的替代方案,请参考 API 文档。
angle_version = 'oc' # 旋转定义方式
model = dict(
type='RotatedRetinaNet', # 检测器(detector)名称
backbone=dict( # 主干网络的配置文件
type='ResNet', # # 主干网络的类别
depth=50, # 主干网络的深度
num_stages=4, # 主干网络阶段(stages)的数目
out_indices=(0, 1, 2, 3), # 每个阶段产生的特征图输出的索引
frozen_stages=1, # 第一个阶段的权重被冻结
zero_init_residual=False, # 是否对残差块(resblocks)中的最后一个归一化层使用零初始化(zero init)让它们表现为自身
norm_cfg=dict( # 归一化层(norm layer)的配置项
type='BN', # 归一化层的类别,通常是 BN 或 GN
requires_grad=True), # 是否训练归一化里的 gamma 和 beta
norm_eval=True, # 是否冻结 BN 里的统计项
style='pytorch', # 主干网络的风格,'pytorch' 意思是步长为2的层为 3x3 卷积, 'caffe' 意思是步长为2的层为 1x1 卷积。
init_cfg=dict(type='Pretrained', checkpoint='torchvision://resnet50')), # 加载通过 ImageNet 预训练的模型
neck=dict(
type='FPN', # 检测器的 neck 是 FPN, 我们同样支持 'ReFPN'
in_channels=[256, 512, 1024, 2048], # 输入通道数,这与主干网络的输出通道一致
out_channels=256, # 金字塔特征图每一层的输出通道
start_level=1, # 用于构建特征金字塔的主干网络起始输入层索引值
add_extra_convs='on_input', # 决定是否在原始特征图之上添加卷积层
num_outs=5), # 决定输出多少个尺度的特征图(scales)
bbox_head=dict(
type='RotatedRetinaHead',# bbox_head 的类型是 'RRetinaHead'
num_classes=15, # 分类的类别数量
in_channels=256, # bbox head 输入通道数
stacked_convs=4, # head 卷积层的层数
feat_channels=256, # head 卷积层的特征通道
assign_by_circumhbbox='oc', # obb2hbb 的旋转定义方式
anchor_generator=dict( # 锚点(Anchor)生成器的配置
type='RotatedAnchorGenerator', # 锚点生成器类别
octave_base_scale=4, # RetinaNet 用于生成锚点的超参数,特征图 anchor 的基本尺度。值越大,所有 anchor 的尺度都会变大。
scales_per_octave=3, # RetinaNet 用于生成锚点的超参数,每个特征图有3个尺度
ratios=[1.0, 0.5, 2.0], # 高度和宽度之间的比率
strides=[8, 16, 32, 64, 128]), # 锚生成器的步幅。这与 FPN 特征步幅一致。如果未设置 base_sizes,则当前步幅值将被视为 base_sizes。
bbox_coder=dict( # 在训练和测试期间对框进行编码和解码
type='DeltaXYWHAOBBoxCoder', # 框编码器的类别
angle_range='oc', # 框编码器的旋转定义方式
norm_factor=None, # 框编码器的范数
edge_swap=False, # 设置是否启用框编码器的边缘交换
proj_xy=False, # 设置是否启用框编码器的投影
target_means=(0.0, 0.0, 0.0, 0.0, 0.0), # 用于编码和解码框的目标均值
target_stds=(1.0, 1.0, 1.0, 1.0, 1.0)), # 用于编码和解码框的标准差
loss_cls=dict( # 分类分支的损失函数配置
type='FocalLoss', # 分类分支的损失函数类型
use_sigmoid=True, # 是否使用 sigmoid
gamma=2.0, # Focal Loss 用于解决难易不均衡的参数 gamma
alpha=0.25, # Focal Loss 用于解决样本数量不均衡的参数 alpha
loss_weight=1.0), # 分类分支的损失权重
loss_bbox=dict( # 回归分支的损失函数配置
type='L1Loss', # 回归分支的损失类型
loss_weight=1.0)), # 回归分支的损失权重
train_cfg=dict( # 训练超参数的配置
assigner=dict( # 分配器(assigner)的配置
type='MaxIoUAssigner', # 分配器的类型
pos_iou_thr=0.5, # IoU >= 0.5(阈值) 被视为正样本
neg_iou_thr=0.4, # IoU < 0.4(阈值) 被视为负样本
min_pos_iou=0, # 将框作为正样本的最小 IoU 阈值
ignore_iof_thr=-1, # 忽略 bbox 的 IoF 阈值
iou_calculator=dict(type='RBboxOverlaps2D')), # IoU 的计算器类型
allowed_border=-1, # 填充有效锚点(anchor)后允许的边框
pos_weight=-1, # 训练期间正样本的权重
debug=False), # 是否设置调试(debug)模式
test_cfg=dict( # 测试超参数的配置
nms_pre=2000, # NMS 前的 box 数
min_bbox_size=0, # box 允许的最小尺寸
score_thr=0.05, # bbox 的分数阈值
nms=dict(iou_thr=0.1), # NMS 的阈值
max_per_img=2000)) # 每张图像的最大检测次数
dataset_type = 'DOTADataset' # 数据集类型,这将被用来定义数据集
data_root = '../datasets/split_1024_dota1_0/' # 数据的根路径
img_norm_cfg = dict( # 图像归一化配置,用来归一化输入的图像
mean=[123.675, 116.28, 103.53], # 预训练里用于预训练主干网络模型的平均值
std=[58.395, 57.12, 57.375], # 预训练里用于预训练主干网络模型的标准差
to_rgb=True) # 预训练里用于预训练主干网络的图像的通道顺序
train_pipeline = [ # 训练流程
dict(type='LoadImageFromFile'), # 第 1 个流程,从文件路径里加载图像
dict(type='LoadAnnotations', # 第 2 个流程,对于当前图像,加载它的注释信息
with_bbox=True), # 是否加载标注框(bounding box), 目标检测需要设置为 True
dict(type='RResize', # 变化图像和其注释大小的数据增广的流程
img_scale=(1024, 1024)), # 图像的最大规模
dict(type='RRandomFlip', # 翻转图像和其注释大小的数据增广的流程
flip_ratio=0.5, # 翻转图像的概率
version='oc'), # 定义旋转的方式
dict(
type='Normalize', # 归一化当前图像的数据增广的流程
mean=[123.675, 116.28, 103.53], # 这些键与 img_norm_cfg 一致,
std=[58.395, 57.12, 57.375], # 因为 img_norm_cfg 被用作参数
to_rgb=True),
dict(type='Pad', # 填充当前图像到指定大小的数据增广的流程
size_divisor=32), # 填充图像可以被当前值整除
dict(type='DefaultFormatBundle'), # 流程里收集数据的默认格式包
dict(type='Collect', # 决定数据中哪些键应该传递给检测器的流程
keys=['img', 'gt_bboxes', 'gt_labels'])
]
test_pipeline = [ # 测试流程
dict(type='LoadImageFromFile'), # 第 1 个流程,从文件路径里加载图像
dict(
type='MultiScaleFlipAug', # 封装测试时数据增广(test time augmentations)
img_scale=(1024, 1024), # 决定测试时可改变图像的最大规模。用于改变图像大小的流程
flip=False, # 测试时是否翻转图像
transforms=[
dict(type='RResize'), # 使用改变图像大小的数据增广
dict(
type='Normalize', # 归一化配置项,值来自 img_norm_cfg
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='Pad', # 将配置传递给可被 32 整除的图像
size_divisor=32),
dict(type='DefaultFormatBundle'), # 用于在管道中收集数据的默认格式包
dict(type='Collect', # 收集测试时必须的键的收集流程
keys=['img'])
])
]
data = dict(
samples_per_gpu=2, # 单个 GPU 的 Batch size
workers_per_gpu=2, # 单个 GPU 分配的数据加载线程数
train=dict( # 训练数据集配置
type='DOTADataset', # 数据集的类别
ann_file=
'../datasets/split_1024_dota1_0/trainval/annfiles/', # 注释文件路径
img_prefix=
'../datasets/split_1024_dota1_0/trainval/images/', # 图片路径前缀
pipeline=[ # 流程, 这是由之前创建的 train_pipeline 传递的
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True),
dict(type='RResize', img_scale=(1024, 1024)),
dict(type='RRandomFlip', flip_ratio=0.5, version='oc'),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='Pad', size_divisor=32),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
],
version='oc'),
val=dict( # 验证数据集的配置
type='DOTADataset',
ann_file=
'../datasets/split_1024_dota1_0/trainval/annfiles/',
img_prefix=
'../datasets/split_1024_dota1_0/trainval/images/',
pipeline=[
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(1024, 1024),
flip=False,
transforms=[
dict(type='RResize'),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='Pad', size_divisor=32),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img'])
])
],
version='oc'),
test=dict( # 测试数据集配置,修改测试开发/测试(test-dev/test)提交的 ann_file
type='DOTADataset',
ann_file=
'../datasets/split_1024_dota1_0/test/images/',
img_prefix=
'../datasets/split_1024_dota1_0/test/images/',
pipeline=[ # 由之前创建的 test_pipeline 传递的流程
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(1024, 1024),
flip=False,
transforms=[
dict(type='RResize'),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='Pad', size_divisor=32),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img'])
])
],
version='oc'))
evaluation = dict( # evaluation hook 的配置
interval=12, # 验证的间隔
metric='mAP') # 验证期间使用的指标
optimizer = dict( # 用于构建优化器的配置文件
type='SGD', # 优化器类型
lr=0.0025, # 优化器的学习率
momentum=0.9, # 动量(Momentum)
weight_decay=0.0001) # SGD 的衰减权重(weight decay)
optimizer_config = dict( # optimizer hook 的配置文件
grad_clip=dict(
max_norm=35,
norm_type=2))
lr_config = dict( # 学习率调整配置,用于注册 LrUpdater hook
policy='step', # 调度流程(scheduler)的策略
warmup='linear', # 预热(warmup)策略,也支持 `exp` 和 `constant`
warmup_iters=500, # 预热的迭代次数
warmup_ratio=0.3333333333333333, # 用于预热的起始学习率的比率
step=[8, 11]) # 衰减学习率的起止回合数
runner = dict(
type='EpochBasedRunner', # 将使用的 runner 的类别 (例如 IterBasedRunner 或 EpochBasedRunner)
max_epochs=12) # runner 总回合(epoch)数, 对于 IterBasedRunner 使用 `max_iters`
checkpoint_config = dict( # checkpoint hook 的配置文件
interval=12) # 保存的间隔是 12
log_config = dict( # register logger hook 的配置文件
interval=50, # 打印日志的间隔
hooks=[
# dict(type='TensorboardLoggerHook') # 同样支持 Tensorboard 日志
dict(type='TextLoggerHook')
]) # 用于记录训练过程的记录器(logger)
dist_params = dict(backend='nccl') # 用于设置分布式训练的参数,端口也同样可被设置
log_level = 'INFO' # 日志的级别
load_from = None # 从一个给定路径里加载模型作为预训练模型,它并不会消耗训练时间
resume_from = None # 从给定路径里恢复检查点(checkpoints),训练模式将从检查点保存的轮次开始恢复训练。
workflow = [('train', 1)] # runner 的工作流程,[('train', 1)] 表示只有一个工作流且工作流仅执行一次。根据 total_epochs 工作流训练 12 个回合(epoch)。
work_dir = './work_dirs/rotated_retinanet_hbb_r50_fpn_1x_dota_oc' # 用于保存当前实验的模型检查点(checkpoints)和日志的目录
常见问题 (FAQ)¶
使用配置文件里的中间变量¶
配置文件里会使用一些中间变量,例如数据集里的 train_pipeline
/test_pipeline
。
值得注意的是,在修改子配置中的中间变量时,需要再次将中间变量传递到相应的字段中。
例如,我们想使用离线多尺度策略 (multi scale strategy)来训练 RoI-Trans。 train_pipeline
是我们想要修改的中间变量。
_base_ = ['./roi-trans-le90_r50_fpn_1x_dota.py']
data_root = '../datasets/split_ms_dota1_0/'
angle_version = 'le90'
img_norm_cfg = dict(
mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
train_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True),
dict(type='RResize', img_scale=(1024, 1024)),
dict(
type='RRandomFlip',
flip_ratio=[0.25, 0.25, 0.25],
direction=['horizontal', 'vertical', 'diagonal'],
version=angle_version),
dict(
type='PolyRandomRotate',
rotate_ratio=0.5,
angles_range=180,
auto_bound=False,
version=angle_version),
dict(type='Normalize', **img_norm_cfg),
dict(type='Pad', size_divisor=32),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
]
data = dict(
train=dict(
pipeline=train_pipeline,
ann_file=data_root + 'trainval/annfiles/',
img_prefix=data_root + 'trainval/images/'),
val=dict(
ann_file=data_root + 'trainval/annfiles/',
img_prefix=data_root + 'trainval/images/'),
test=dict(
ann_file=data_root + 'test/images/',
img_prefix=data_root + 'test/images/'))
我们首先定义新的 train_pipeline
/test_pipeline
然后传递到 data
里。
同样的,如果我们想从 SyncBN
切换到 BN
或者 MMSyncBN
,我们需要修改配置文件里的每一个 norm_cfg
。
_base_ = './roi-trans-le90_r50_fpn_1x_dota.py'
norm_cfg = dict(type='BN', requires_grad=True)
model = dict(
backbone=dict(norm_cfg=norm_cfg),
neck=dict(norm_cfg=norm_cfg),
...)
训练 & 测试 (待更新)¶
测试一个模型¶
单个 GPU
单个节点多个 GPU
多个节点多个 GPU
您可以使用以下命令来推理数据集。
# 单个 GPU
python tools/test.py ${CONFIG_FILE} ${CHECKPOINT_FILE} [optional arguments]
# 多个 GPU
./tools/dist_test.sh ${CONFIG_FILE} ${CHECKPOINT_FILE} ${GPU_NUM} [optional arguments]
# slurm 环境中多个节点
python tools/test.py ${CONFIG_FILE} ${CHECKPOINT_FILE} [optional arguments] --launcher slurm
例子:
在 DOTA-1.0 数据集推理 RotatedRetinaNet 并生成压缩文件用于在线提交 (首先请修改 data_root)。
python ./tools/test.py \
configs/rotated_retinanet/rotated-retinanet-rbox-le90_r50_fpn_1x_dota.py \
checkpoints/SOME_CHECKPOINT.pth --format-only \
--eval-options submission_dir=work_dirs/Task1_results
或者
./tools/dist_test.sh \
configs/rotated_retinanet/rotated-retinanet-rbox-le90_r50_fpn_1x_dota.py \
checkpoints/SOME_CHECKPOINT.pth 1 --format-only \
--eval-options submission_dir=work_dirs/Task1_results
您可以修改 data_root 中测试集的路径为验证集或训练集路径用于离线的验证。
python ./tools/test.py \
configs/rotated_retinanet/rotated-retinanet-rbox-le90_r50_fpn_1x_dota.py \
checkpoints/SOME_CHECKPOINT.pth --eval mAP
或者
./tools/dist_test.sh \
configs/rotated_retinanet/rotated-retinanet-rbox-le90_r50_fpn_1x_dota.py \
checkpoints/SOME_CHECKPOINT.pth 1 --eval mAP
您也可以可视化结果。
python ./tools/test.py \
configs/rotated_retinanet/rotated-retinanet-rbox-le90_r50_fpn_1x_dota.py \
checkpoints/SOME_CHECKPOINT.pth \
--show-dir work_dirs/vis
训练一个模型¶
单 GPU 训练¶
python tools/train.py ${CONFIG_FILE} [optional arguments]
如果您想在命令行中指定工作路径,您可以增加参数 --work_dir ${YOUR_WORK_DIR}
。
多 GPU 训练¶
./tools/dist_train.sh ${CONFIG_FILE} ${GPU_NUM} [optional arguments]
可选参数包括:
--no-validate
(不建议): 默认情况下代码将在训练期间进行评估。通过设置--no-validate
关闭训练期间进行评估。--work-dir ${WORK_DIR}
: 覆盖配置文件中指定的工作目录。--resume-from ${CHECKPOINT_FILE}
: 从以前的检查点恢复训练。
resume-from
和 load-from
的不同点:
resume-from
读取模型的权重和优化器的状态,并且 epoch 也会继承于指定的检查点。通常用于恢复意外中断的训练过程。
load-from
只读取模型的权重并且训练的 epoch 会从 0 开始。通常用于微调。
使用多台机器训练¶
如果您想使用由 ethernet 连接起来的多台机器, 您可以使用以下命令:
在第一台机器上:
NNODES=2 NODE_RANK=0 PORT=$MASTER_PORT MASTER_ADDR=$MASTER_ADDR sh tools/dist_train.sh $CONFIG $GPUS
在第二台机器上:
NNODES=2 NODE_RANK=1 PORT=$MASTER_PORT MASTER_ADDR=$MASTER_ADDR sh tools/dist_train.sh $CONFIG $GPUS
但是,如果您不使用高速网路连接这几台机器的话,训练将会非常慢。
使用 Slurm 来管理任务¶
如果您在 slurm 管理的集群上运行 MMRotate,您可以使用脚本 slurm_train.sh
(此脚本还支持单机训练)。
[GPUS=${GPUS}] ./tools/slurm_train.sh ${PARTITION} ${JOB_NAME} ${CONFIG_FILE} ${WORK_DIR}
如果您有多台机器联网,您可以参考 PyTorch launch utility。 如果您没有像 InfiniBand 这样的高速网络,训练速度通常会很慢。
在一台机器上启动多个作业¶
如果您在一台机器上启动多个作业,如在一台机器上使用 8 张 GPU 训练 2 个作业,每个作业使用 4 张 GPU ,您需要为每个作业指定不同的端口号(默认为 29500 )进而避免通讯冲突。
如果您使用 dist_train.sh
启动训练,您可以在命令行中指定端口号。
CUDA_VISIBLE_DEVICES=0,1,2,3 PORT=29500 ./tools/dist_train.sh ${CONFIG_FILE} 4
CUDA_VISIBLE_DEVICES=4,5,6,7 PORT=29501 ./tools/dist_train.sh ${CONFIG_FILE} 4
如果您通过 Slurm 启动训练,您需要修改配置文件(通常是配置文件底部的第 6 行)进而设置不同的通讯端口。
在 config1.py
中,
dist_params = dict(backend='nccl', port=29500)
在 config2.py
中,
dist_params = dict(backend='nccl', port=29501)
之后您可以使用 config1.py
和 config2.py
开启两个作业。
CUDA_VISIBLE_DEVICES=0,1,2,3 GPUS=4 ./tools/slurm_train.sh ${PARTITION} ${JOB_NAME} config1.py ${WORK_DIR}
CUDA_VISIBLE_DEVICES=4,5,6,7 GPUS=4 ./tools/slurm_train.sh ${PARTITION} ${JOB_NAME} config2.py ${WORK_DIR}
实用工具¶
实用工具 (待更新)¶
除了训练和测试脚本,我们在 tools/
文件夹内还提供了一些有用的工具。
日志分析¶
tools/analysis_tools/analyze_logs.py
通过给定的日志文件绘制 loss/mAP 曲线。 需首先执行 pip install seaborn
安装依赖。
python tools/analysis_tools/analyze_logs.py plot_curve [--keys ${KEYS}] [--title ${TITLE}] [--legend ${LEGEND}] [--backend ${BACKEND}] [--style ${STYLE}] [--out ${OUT_FILE}]
示例:
绘制某次执行的分类损失
python tools/analysis_tools/analyze_logs.py plot_curve log.json --keys loss_cls --legend loss_cls
绘制某次执行的分类和回归损失,同时将图像保存到 pdf 文件
python tools/analysis_tools/analyze_logs.py plot_curve log.json --keys loss_cls loss_bbox --out losses.pdf
在同一张图像中比较两次执行的 mAP
python tools/analysis_tools/analyze_logs.py plot_curve log1.json log2.json --keys bbox_mAP --legend run1 run2
计算平均训练速度
python tools/analysis_tools/analyze_logs.py cal_train_time log.json [--include-outliers]
预计输出如下
-----Analyze train time of work_dirs/some_exp/20190611_192040.log.json----- slowest epoch 11, average time is 1.2024 fastest epoch 1, average time is 1.1909 time std over epochs is 0.0028 average iter time: 1.1959 s/iter
可视化¶
可视化数据集¶
tools/misc/browse_dataset.py
帮助用户浏览检测的数据集(包括图像和检测框的标注),或将图像保存到指定目录。
python tools/misc/browse_dataset.py ${CONFIG} [-h] [--skip-type ${SKIP_TYPE[SKIP_TYPE...]}] [--output-dir ${OUTPUT_DIR}] [--not-show] [--show-interval ${SHOW_INTERVAL}]
模型部署¶
为了使用 TorchServe
部署一个 MMRotate
模型,需要进行以下几步:
1. 转换 MMRotate 模型至 TorchServe¶
python tools/deployment/mmrotate2torchserve.py ${CONFIG_FILE} ${CHECKPOINT_FILE} \
--output-folder ${MODEL_STORE} \
--model-name ${MODEL_NAME}
示例:
wget -P checkpoint \
https://download.openmmlab.com/mmrotate/v0.1.0/rotated_faster_rcnn/rotated-faster-rcnn-le90_r50_fpn_1x_dota/rotated_faster_rcnn_r50_fpn_1x_dota_le90-0393aa5c.pth
python tools/deployment/mmrotate2torchserve.py configs/rotated_faster_rcnn/rotated-faster-rcnn-le90_r50_fpn_1x_dota.py checkpoint/rotated_faster_rcnn_r50_fpn_1x_dota_le90-0393aa5c.pth \
--output-folder ${MODEL_STORE} \
--model-name rotated_faster_rcnn
Note: ${MODEL_STORE} 需要是一个文件夹的绝对路径。
2. 构建 mmrotate-serve
docker 镜像¶
docker build -t mmrotate-serve:latest docker/serve/
3. 运行 mmrotate-serve
镜像¶
请参考官方文档 基于 docker 运行 TorchServe.
为了使镜像能够使用 GPU 资源,需要安装 nvidia-docker。之后可以传递 --gpus
参数以在 GPU 上运行。
示例:
docker run --rm \
--cpus 8 \
--gpus device=0 \
-p8080:8080 -p8081:8081 -p8082:8082 \
--mount type=bind,source=$MODEL_STORE,target=/home/model-server/model-store \
mmrotate-serve:latest
参考 该文档 了解关于推理 (8080),管理 (8081) 和指标 (8082) 等 API 的信息。
4. 测试部署¶
curl -O https://raw.githubusercontent.com/open-mmlab/mmrotate/main/demo/demo.jpg
curl http://127.0.0.1:8080/predictions/${MODEL_NAME} -T demo.jpg
您应该获得类似于以下内容的响应:
[
{
"class_name": "small-vehicle",
"bbox": [
584.9473266601562,
327.2749938964844,
38.45665740966797,
16.898427963256836,
-0.7229751944541931
],
"score": 0.9766026139259338
},
{
"class_name": "small-vehicle",
"bbox": [
152.0239715576172,
305.92572021484375,
43.144744873046875,
18.85024642944336,
0.014928221702575684
],
"score": 0.972826361656189
},
{
"class_name": "large-vehicle",
"bbox": [
160.58056640625,
437.3690185546875,
55.6795654296875,
19.31710433959961,
0.007036328315734863
],
"score": 0.888836681842804
},
{
"class_name": "large-vehicle",
"bbox": [
666.2868041992188,
1011.3961181640625,
60.396209716796875,
21.821645736694336,
0.8549195528030396
],
"score": 0.8240180015563965
}
]
另外,你也可以使用 test_torchserver.py
来比较 TorchServe 和 PyTorch 的结果,并进行可视化。
python tools/deployment/test_torchserver.py ${IMAGE_FILE} ${CONFIG_FILE} ${CHECKPOINT_FILE} ${MODEL_NAME}
[--inference-addr ${INFERENCE_ADDR}] [--device ${DEVICE}] [--score-thr ${SCORE_THR}]
示例:
python tools/deployment/test_torchserver.py \
demo/demo.jpg \
configs/rotated_faster_rcnn/rotated-faster-rcnn-le90_r50_fpn_1x_dota.py \
rotated_faster_rcnn_r50_fpn_1x_dota_le90-0393aa5c.pth \
rotated_fater_rcnn
模型复杂度¶
tools/analysis_tools/get_flops.py
是改编自 flops-counter.pytorch 的脚本,用于计算给定模型的 FLOPs 和参数量。
python tools/analysis_tools/get_flops.py ${CONFIG_FILE} [--shape ${INPUT_SHAPE}]
预计输出如下
==============================
Input shape: (3, 1024, 1024)
Flops: 215.92 GFLOPs
Params: 36.42 M
==============================
注意: 此工具仍处于实验阶段,我们并不能保证计算结果是绝对正确的。你可以将结果用于简单的比较, 但在技术报告或论文中采用之前请仔细检查
FLOPs 与输入大小相关,但参数量与其无关。默认输入大小是(1, 3, 1024, 1024)。
一些算子例如 DCN 或自定义算子并未包含在 FLOPs 计算中,所以 S2A-Net 和基于 RepPoints 的模型的 FLOPs 计算是错误的。 详细信息请查看
mmcv.cnn.get_model_complexity_info()
。两阶段检测器的 FLOPs 取决于候选的数量。
准备发布模型¶
tools/model_converters/publish_model.py
帮助用户准备他们将发布的模型。
在将模型上传到 AWS 之前,你可能需要
将模型权重转换至 CPU
删除优化器的状态
计算权重文件的哈希值并附加到文件名后
python tools/model_converters/publish_model.py ${INPUT_FILENAME} ${OUTPUT_FILENAME}
例如,
python tools/model_converters/publish_model.py work_dirs/rotated_faster_rcnn/latest.pth rotated_faster_rcnn_r50_fpn_1x_dota_le90_20190801.pth
最终输出的文件名是 rotated_faster_rcnn_r50_fpn_1x_dota_le90_20190801-{hash id}.pth
。
基准测试¶
FPS 基准¶
tools/analysis_tools/benchmark.py
帮助用户计算 FPS。 FPS 值包括模型前向传播和后处理。为了得到更准确的数值,目前只支持单 GPU 分布式启动。
python -m torch.distributed.launch --nproc_per_node=1 --master_port=${PORT} tools/analysis_tools/benchmark.py \
${CONFIG} \
${CHECKPOINT} \
[--repeat-num ${REPEAT_NUM}] \
[--max-iter ${MAX_ITER}] \
[--log-interval ${LOG_INTERVAL}] \
--launcher pytorch
示例: 假设你已经下载了 Rotated Faster R-CNN
模型权重到 checkpoints/
文件夹
python -m torch.distributed.launch --nproc_per_node=1 --master_port=29500 tools/analysis_tools/benchmark.py \
configs/rotated_faster_rcnn/rotated-faster-rcnn-le90_r50_fpn_1x_dota.py \
checkpoints/rotated_faster_rcnn_r50_fpn_1x_dota_le90-0393aa5c.pth \
--launcher pytorch
杂项¶
打印完整配置文件¶
tools/misc/print_config.py
输出整个配置文件并整合其所有导入。
python tools/misc/print_config.py ${CONFIG} [-h] [--options ${OPTIONS [OPTIONS...]}]
混淆矩阵¶
混淆矩阵是预测结果的概要
tools/analysis_tools/confusion_matrix.py
可以分析预测结果并绘制混淆矩阵。
首先执行 tools/test.py
将检测结果保存为 .pkl
文件。
之后执行
python tools/analysis_tools/confusion_matrix.py ${CONFIG} ${DETECTION_RESULTS} ${SAVE_DIR} --show
你会得到一个类似于下图的混淆矩阵:
基础概念¶
组件定制¶
自定义模型 (待更新)¶
我们大致将模型组件分为了 5 种类型。
主干网络 (Backbone): 通常是一个全卷积网络 (FCN),用来提取特征图,比如残差网络 (ResNet)。也可以是基于视觉 Transformer 的网络,比如 Swin Transformer 等。
Neck: 主干网络和任务头 (Head) 之间的连接组件,比如 FPN, ReFPN。
任务头 (Head): 用于某种具体任务(比如边界框预测)的组件。
区域特征提取器 (Roi Extractor): 用于从特征图上提取区域特征的组件,比如 RoI Align Rotated。
损失 (loss): 任务头上用于计算损失函数的组件,比如 FocalLoss, GWDLoss, and KFIoULoss。
开发新的组件¶
添加新的主干网络¶
这里,我们以 MobileNet 为例来展示如何开发新组件。
1. 定义一个新的主干网络(以 MobileNet 为例)¶
新建文件 mmrotate/models/backbones/mobilenet.py
。
import torch.nn as nn
from mmrotate.models.builder import ROTATED_BACKBONES
@ROTATED_BACKBONES.register_module()
class MobileNet(nn.Module):
def __init__(self, arg1, arg2):
pass
def forward(self, x): # should return a tuple
pass
2. 导入模块¶
你可以将下面的代码添加到 mmrotate/models/backbones/__init__.py
中:
from .mobilenet import MobileNet
或者添加如下代码
custom_imports = dict(
imports=['mmrotate.models.backbones.mobilenet'],
allow_failed_imports=False)
到配置文件中以避免修改原始代码。
3. 在你的配置文件中使用该主干网络¶
model = dict(
...
backbone=dict(
type='MobileNet',
arg1=xxx,
arg2=xxx),
...
添加新的 Neck¶
1. 定义一个 Neck(以 PAFPN 为例)¶
新建文件 mmrotate/models/necks/pafpn.py
。
from mmrotate.models.builder import ROTATED_NECKS
@ROTATED_NECKS.register_module()
class PAFPN(nn.Module):
def __init__(self,
in_channels,
out_channels,
num_outs,
start_level=0,
end_level=-1,
add_extra_convs=False):
pass
def forward(self, inputs):
# implementation is ignored
pass
2. 导入该模块¶
你可以添加下述代码到 mmrotate/models/necks/__init__.py
中
from .pafpn import PAFPN
或者添加
custom_imports = dict(
imports=['mmrotate.models.necks.pafpn.py'],
allow_failed_imports=False)
到配置文件中以避免修改原始代码。
3. 修改配置文件¶
neck=dict(
type='PAFPN',
in_channels=[256, 512, 1024, 2048],
out_channels=256,
num_outs=5)
添加新的 Head¶
这里,我们以 Double Head R-CNN 为例来展示如何添加一个新的 Head。
首先,添加一个新的 bbox head 到 mmrotate/models/roi_heads/bbox_heads/double_bbox_head.py
。
Double Head R-CNN 在目标检测上实现了一个新的 bbox head。为了实现 bbox head,我们需要使用如下的新模块中三个函数。
from mmrotate.models.builder import ROTATED_HEADS
from mmrotate.models.roi_heads.bbox_heads.bbox_head import BBoxHead
@ROTATED_HEADS.register_module()
class DoubleConvFCBBoxHead(BBoxHead):
r"""Bbox head used in Double-Head R-CNN
/-> cls
/-> shared convs ->
\-> reg
roi features
/-> cls
\-> shared fc ->
\-> reg
""" # noqa: W605
def __init__(self,
num_convs=0,
num_fcs=0,
conv_out_channels=1024,
fc_out_channels=1024,
conv_cfg=None,
norm_cfg=dict(type='BN'),
**kwargs):
kwargs.setdefault('with_avg_pool', True)
super(DoubleConvFCBBoxHead, self).__init__(**kwargs)
def forward(self, x_cls, x_reg):
然后,如有必要,我们需要实现一个新的 RoI Head。我们打算从 StandardRoIHead
继承出新的 DoubleHeadRoIHead
。我们发现 StandardRoIHead
已经实现了下述函数。
import torch
from mmdet.core import bbox2result, bbox2roi, build_assigner, build_sampler
from mmrotate.models.builder import ROTATED_HEADS, build_head, build_roi_extractor
from mmrotate.models.roi_heads.base_roi_head import BaseRoIHead
from mmrotate.models.roi_heads.test_mixins import BBoxTestMixin, MaskTestMixin
@ROTATED_HEADS.register_module()
class StandardRoIHead(BaseRoIHead, BBoxTestMixin, MaskTestMixin):
"""Simplest base roi head including one bbox head and one mask head.
"""
def init_assigner_sampler(self):
def init_bbox_head(self, bbox_roi_extractor, bbox_head):
def forward_dummy(self, x, proposals):
def forward_train(self,
x,
img_metas,
proposal_list,
gt_bboxes,
gt_labels,
gt_bboxes_ignore=None,
gt_masks=None):
def _bbox_forward(self, x, rois):
def _bbox_forward_train(self, x, sampling_results, gt_bboxes, gt_labels,
img_metas):
def simple_test(self,
x,
proposal_list,
img_metas,
proposals=None,
rescale=False):
"""Test without augmentation."""
Double Head 的修改主要在 _bbox_forward 的逻辑中,且它从 StandardRoIHead
中继承了其他逻辑。
在 mmrotate/models/roi_heads/double_roi_head.py
中, 我们实现如下的新的 RoI Head:
from mmrotate.models.builder import ROTATED_HEADS
from mmrotate.models.roi_heads.standard_roi_head import StandardRoIHead
@ROTATED_HEADS.register_module()
class DoubleHeadRoIHead(StandardRoIHead):
"""RoI head for Double Head RCNN
https://arxiv.org/abs/1904.06493
"""
def __init__(self, reg_roi_scale_factor, **kwargs):
super(DoubleHeadRoIHead, self).__init__(**kwargs)
self.reg_roi_scale_factor = reg_roi_scale_factor
def _bbox_forward(self, x, rois):
bbox_cls_feats = self.bbox_roi_extractor(
x[:self.bbox_roi_extractor.num_inputs], rois)
bbox_reg_feats = self.bbox_roi_extractor(
x[:self.bbox_roi_extractor.num_inputs],
rois,
roi_scale_factor=self.reg_roi_scale_factor)
if self.with_shared_head:
bbox_cls_feats = self.shared_head(bbox_cls_feats)
bbox_reg_feats = self.shared_head(bbox_reg_feats)
cls_score, bbox_pred = self.bbox_head(bbox_cls_feats, bbox_reg_feats)
bbox_results = dict(
cls_score=cls_score,
bbox_pred=bbox_pred,
bbox_feats=bbox_cls_feats)
return bbox_results
最后,用户需要把这个新模块添加到 mmrotate/models/bbox_heads/__init__.py
以及 mmrotate/models/roi_heads/__init__.py
中。 这样,注册机制就能找到并加载它们。
另外,用户也可以添加
custom_imports=dict(
imports=['mmrotate.models.roi_heads.double_roi_head', 'mmrotate.models.bbox_heads.double_bbox_head'])
到配置文件中来实现同样的目的。
添加新的损失¶
假设你想添加一个新的损失 MyLoss
用于边界框回归。
为了添加一个新的损失函数,用户需要在 mmrotate/models/losses/my_loss.py
中实现。
装饰器 weighted_loss
可以使损失每个部分加权。
import torch
import torch.nn as nn
from mmrotate.models.builder import ROTATED_LOSSES
from mmdet.models.losses.utils import weighted_loss
@weighted_loss
def my_loss(pred, target):
assert pred.size() == target.size() and target.numel() > 0
loss = torch.abs(pred - target)
return loss
@ROTATED_LOSSES.register_module()
class MyLoss(nn.Module):
def __init__(self, reduction='mean', loss_weight=1.0):
super(MyLoss, self).__init__()
self.reduction = reduction
self.loss_weight = loss_weight
def forward(self,
pred,
target,
weight=None,
avg_factor=None,
reduction_override=None):
assert reduction_override in (None, 'none', 'mean', 'sum')
reduction = (
reduction_override if reduction_override else self.reduction)
loss_bbox = self.loss_weight * my_loss(
pred, target, weight, reduction=reduction, avg_factor=avg_factor)
return loss_bbox
然后,用户需要把下面的代码加到 mmrotate/models/losses/__init__.py
中。
from .my_loss import MyLoss, my_loss
或者,你可以添加:
custom_imports=dict(
imports=['mmrotate.models.losses.my_loss'])
到配置文件来实现相同的目的。
因为 MyLoss 是用于回归的,你需要在 Head 中修改 loss_bbox
字段:
loss_bbox=dict(type='MyLoss', loss_weight=1.0))
自定义数据集 (待更新)¶
支持新的数据格式¶
要支持新的数据格式,您可以将它们转换为现有的格式(DOTA 格式)。您可以选择离线(在通过脚本训练之前)或在线(实施新数据集并在训练时进行转换)进行转换。 在 MMRotate 中,我们建议将数据转换为 DOTA 格式并离线进行转换,如此您只需在数据转换后修改 config 的数据标注路径和类别即可。
将新数据格式重构为现有格式¶
最简单的方法是将数据集转换为现有数据集格式 (DOTA) 。
DOTA 格式的注解 txt 文件:
184 2875 193 2923 146 2932 137 2885 plane 0
66 2095 75 2142 21 2154 11 2107 plane 0
...
每行代表一个对象,并将其记录为一个 10 维数组 A
。
A[0:8]
: 多边形的格式(x1, y1, x2, y2, x3, y3, x4, y4)
。A[8]
: 类别A[9]
: 困难
在数据预处理之后,用户可以通过两个步骤来训练具有现有格式(例如 DOTA 格式)的自定义新数据集:
修改配置文件以使用自定义数据集。
检查自定义数据集的标注。
下面给出两个例子展示上述两个步骤,它使用一个自定义的 5 类 COCO 格式的数据集来训练一个现有的 Cascade Mask R-CNN R50-FPN 检测器。
1. 修改配置文件以使用自定义数据集¶
配置文件的修改主要涉及两个方面:
data
部分。具体来说,您需要在data.train
,data.val
和data.test
中显式添加 classes 字段。data
属性变量。具体来说,特别是您需要在data.train
,data.val
和data.test
中添加 classes 字段。model
部分中的num_classes
属性变量。特别是将所有 num_classes 的默认值(例如 COCO 中的 80)覆盖到您的类别编号中。
在 configs/my_custom_config.py
:
# 新配置继承了基础配置用于突出显示必要的修改
_base_ = './rotated_retinanet_hbb_r50_fpn_1x_dota_oc'
# 1. 数据集的设置
dataset_type = 'DOTADataset'
classes = ('a', 'b', 'c', 'd', 'e')
data = dict(
samples_per_gpu=2,
workers_per_gpu=2,
train=dict(
type=dataset_type,
# 注意将你的类名添加到字段 `classes`
classes=classes,
ann_file='path/to/your/train/annotation_data',
img_prefix='path/to/your/train/image_data'),
val=dict(
type=dataset_type,
# 注意将你的类名添加到字段 `classes`
classes=classes,
ann_file='path/to/your/val/annotation_data',
img_prefix='path/to/your/val/image_data'),
test=dict(
type=dataset_type,
# 注意将你的类名添加到字段 `classes`
classes=classes,
ann_file='path/to/your/test/annotation_data',
img_prefix='path/to/your/test/image_data'))
# 2. 模型设置
model = dict(
bbox_head=dict(
type='RotatedRetinaHead',
# 显式将所有 `num_classes` 字段从 15 重写为 5。。
num_classes=15))
2. 查看自定义数据集的标注¶
假设您的自定义数据集是 DOTA 格式,请确保您在自定义数据集中具有正确的标注:
配置文件中的
classes
字段应该与 txt 标注的A[8]
保持完全相同的元素和相同的顺序。 MMRotate 会自动的将categories
中不连续的id
映射到连续的标签索引中,所以在categories
中name
的字符串顺序会影响标签索引的顺序。同时,配置文件中classes
的字符串顺序也会影响预测边界框可视化过程中的标签文本信息。
通过封装器自定义数据集¶
MMRotate 还支持许多数据集封装器对数据集进行混合或修改数据集的分布以进行训练。目前它支持三个数据集封装器,如下所示:
RepeatDataset
: 简单地重复整个数据集。ClassBalancedDataset
: 以类平衡的方式重复数据集。ConcatDataset
: 拼接数据集。
重复数据集¶
我们使用 RepeatDataset
作为封装器来重复这个数据集。例如,假设原始数据集是 Dataset_A
,我们就重复一遍这个数据集。配置信息如下所示:
dataset_A_train = dict(
type='RepeatDataset',
times=N,
dataset=dict( # 这是 Dataset_A 的原始配置信息
type='Dataset_A',
...
pipeline=train_pipeline
)
)
类别平衡数据集¶
我们使用 ClassBalancedDataset
作为封装器,根据类别频率重复数据集。这个数据集的重复操作 ClassBalancedDataset
需要实例化函数 self.get_cat_ids(idx)
的支持。例如,Dataset_A
需要使用oversample_thr=1e-3
,配置信息如下所示:
dataset_A_train = dict(
type='ClassBalancedDataset',
oversample_thr=1e-3,
dataset=dict( # This is the original config of Dataset_A
type='Dataset_A',
...
pipeline=train_pipeline
)
)
拼接数据集¶
这里用三种方式对数据集进行拼接。
如果要拼接的数据集属于同一类型且具有不同的标注文件,则可以通过如下所示的配置信息来拼接数据集:
dataset_A_train = dict( type='Dataset_A', ann_file = ['anno_file_1', 'anno_file_2'], pipeline=train_pipeline ) ```如果拼接后的数据集用于测试或评估,我们这种方式是可以支持对每个数据集分别进行评估。如果要测试的整个拼接数据集,如下所示您可以直接通过设置 separate_eval=False 来实现。 ```python dataset_A_train = dict( type='Dataset_A', ann_file = ['anno_file_1', 'anno_file_2'], separate_eval=False, pipeline=train_pipeline )
如果您要拼接不同的数据集,您可以通过如下所示的方法对拼接数据集配置信息进行设置。
dataset_A_train = dict() dataset_B_train = dict() data = dict( imgs_per_gpu=2, workers_per_gpu=2, train = [ dataset_A_train, dataset_B_train ], val = dataset_A_val, test = dataset_A_test ) ```如果拼接后的数据集用于测试或评估,这种方式还支持对每个数据集分别进行评估。
我们也支持如下所示的方法对
ConcatDataset
进行明确的定义。dataset_A_val = dict() dataset_B_val = dict() data = dict( imgs_per_gpu=2, workers_per_gpu=2, train=dataset_A_train, val=dict( type='ConcatDataset', datasets=[dataset_A_val, dataset_B_val], separate_eval=False)) ```这种方式允许用户通过设置 `separate_eval=False` 将所有数据集转为单个数据集进行评估。
笔记:
假设数据集在评估期间使用
self.data_infos
,就要把选项设置为separate_eval=False
。因为 COCO 数据集不完全依赖self.data_infos
进行评估,所以 COCO 数据集并不支持这种设置操作。没有在组合不同类型的数据集并对其进行整体评估的场景进行测试,因此我们不建议使用这样的操作。不支持评估
ClassBalancedDataset
和RepeatDataset
,所以也不支持评估这些类型的串联组合后的数据集。
一个更复杂的例子,分别将 Dataset_A
和 Dataset_B
重复 N 次和 M 次,然后将重复的数据集连接起来,如下所示。
dataset_A_train = dict(
type='RepeatDataset',
times=N,
dataset=dict(
type='Dataset_A',
...
pipeline=train_pipeline
)
)
dataset_A_val = dict(
...
pipeline=test_pipeline
)
dataset_A_test = dict(
...
pipeline=test_pipeline
)
dataset_B_train = dict(
type='RepeatDataset',
times=M,
dataset=dict(
type='Dataset_B',
...
pipeline=train_pipeline
)
)
data = dict(
imgs_per_gpu=2,
workers_per_gpu=2,
train = [
dataset_A_train,
dataset_B_train
],
val = dataset_A_val,
test = dataset_A_test
)
自定义训练设置 (待更新)¶
自定义优化设置¶
自定义 Pytorch 支持的优化器¶
我们已经支持了全部 Pytorch 自带的优化器,唯一需要修改的就是配置文件中 optimizer
部分。
例如,如果您想使用 ADAM
(注意如下操作可能会让模型表现下降),可以使用如下修改:
optimizer = dict(type='Adam', lr=0.0003, weight_decay=0.0001)
为了修改模型训练的学习率,使用者仅需修改配置文件里 optimizer
的 lr
即可。
使用者可以参考 PyTorch 的 API doc 直接设置参数。
自定义用户自己实现的优化器¶
1. 定义一个新的优化器¶
一个自定义的优化器可以这样定义:
假如您想增加一个叫做 MyOptimizer
的优化器,它的参数分别有 a
, b
, 和 c
。
您需要创建一个名为 mmrotate/core/optimizer
的新文件夹;然后参考如下代码段在 mmrotate/core/optimizer/my_optimizer.py
文件中实现新的优化器:
from mmdet.core.optimizer.registry import OPTIMIZERS
from torch.optim import Optimizer
@OPTIMIZERS.register_module()
class MyOptimizer(Optimizer):
def __init__(self, a, b, c)
2. 增加优化器到注册表 (registry)¶
为了能够使得上述添加的模块被 mmrotate
发现,需要先将该模块添加到主命名空间(main namespace)。
修改
mmrotate/core/optimizer/__init__.py
文件来导入该模块。新的被定义的模块应该被导入到
mmrotate/core/optimizer/__init__.py
中,这样注册表才会发现新的模块并添加它:
from .my_optimizer import MyOptimizer
在配置文件中使用
custom_imports
来手动添加该模块
custom_imports = dict(imports=['mmrotate.core.optimizer.my_optimizer'], allow_failed_imports=False)
mmrotate.core.optimizer.my_optimizer
模块将会在程序开始被导入,并且 MyOptimizer
类将会自动注册。
需要注意只有包含 MyOptimizer
类的包 (package) 应当被导入。
而 mmrotate.core.optimizer.my_optimizer.MyOptimizer
不能 被直接导入。
事实上,在这种导入方式下用户可以用完全不同的文件夹结构,只要这一模块的根目录已经被添加到 PYTHONPATH
里面。
3. 在配置文件中指定优化器¶
之后您可以在配置文件的 optimizer
部分里面使用 MyOptimizer
。
在配置文件里,优化器按照如下形式被定义在 optimizer
部分里:
optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001)
要使用用户自定义的优化器,这部分应该改成:
optimizer = dict(type='MyOptimizer', a=a_value, b=b_value, c=c_value)
自定义优化器的构造函数 (constructor)¶
有些模型的优化器可能有一些特别参数配置,例如批归一化层 (BatchNorm layers) 的权重衰减系数 (weight decay)。 用户可以通过自定义优化器的构造函数去微调这些细粒度参数。
from mmcv.utils import build_from_cfg
from mmcv.runner.optimizer import OPTIMIZER_BUILDERS, OPTIMIZERS
from mmrotate.utils import get_root_logger
from .my_optimizer import MyOptimizer
@OPTIMIZER_BUILDERS.register_module()
class MyOptimizerConstructor(object):
def __init__(self, optimizer_cfg, paramwise_cfg=None):
def __call__(self, model):
return my_optimizer
mmcv
默认的优化器构造函数实现可以参考 这里 ,这也可以作为新的优化器构造函数的模板。
其他配置¶
优化器未实现的技巧应该通过修改优化器构造函数(如设置基于参数的学习率)或者钩子(hooks)去实现。我们列出一些常见的设置,它们可以稳定或加速模型的训练。 如果您有更多的设置,欢迎在 PR 和 issue 里面提出。
使用梯度裁剪 (gradient clip) 来稳定训练: 一些模型需要梯度裁剪来稳定训练过程。使用方式如下:
optimizer_config = dict( _delete_=True, grad_clip=dict(max_norm=35, norm_type=2))
如果您的配置继承了已经设置了
optimizer_config
的基础配置(base config),你可能需要设置_delete_=True
来覆盖不必要的配置参数。请参考 配置文档 了解更多细节。使用动量调度加速模型收敛: 我们支持动量规划器(Momentum scheduler),以实现根据学习率调节模型优化过程中的动量设置,这可以使模型以更快速度收敛。 动量规划器经常与学习率规划器(LR scheduler)一起使用,例如下面的配置经常被用于 3D 检测模型训练中以加速收敛。更多细节请参考 CyclicLrUpdater 和 CyclicMomentumUpdater。
lr_config = dict( policy='cyclic', target_ratio=(10, 1e-4), cyclic_times=1, step_ratio_up=0.4, ) momentum_config = dict( policy='cyclic', target_ratio=(0.85 / 0.95, 1), cyclic_times=1, step_ratio_up=0.4, )
自定义训练计划¶
默认地,我们使用 1x 计划(1x schedule)的步进学习率(step learning rate),这在 MMCV 中被称为 StepLRHook
。
我们支持很多其他的学习率规划器,参考 这里 ,例如 CosineAnnealing
和 Poly
。下面是一些例子:
Poly
:lr_config = dict(policy='poly', power=0.9, min_lr=1e-4, by_epoch=False)
ConsineAnnealing
:lr_config = dict( policy='CosineAnnealing', warmup='linear', warmup_iters=1000, warmup_ratio=1.0 / 10, min_lr_ratio=1e-5)
自定义工作流 (workflow)¶
工作流是一个专门定义运行顺序和轮数(epochs)的列表。 默认情况下它设置成:
workflow = [('train', 1)]
这是指训练 1 个 epoch。 有时候用户可能想检查一些模型在验证集上的指标,如损失函数值(Loss)和准确性(Accuracy)。 在这种情况下,我们可以将工作流设置为:
[('train', 1), ('val', 1)]
这样以来, 1 个 epoch 训练,1 个 epoch 验证将交替运行。
注意:
模型参数在验证的阶段不会被自动更新。
配置文件里的键值
total_epochs
仅控制训练的 epochs 数目,而不会影响验证工作流。工作流
[('train', 1), ('val', 1)]
和[('train', 1)]
将不会改变EvalHook
的行为,因为EvalHook
被after_train_epoch
调用而且验证的工作流仅仅影响通过调用after_val_epoch
的钩子 (hooks)。因此,[('train', 1), ('val', 1)]
和[('train', 1)]
的区别仅在于 runner 将在每次训练阶段(training epoch)结束后计算在验证集上的损失。
自定义钩 (hooks)¶
自定义用户自己实现的钩子(hooks)¶
1. 实现一个新的钩子(hook)¶
在某些情况下,用户可能需要实现一个新的钩子。 MMRotate 支持训练中的自定义钩子。 因此,用户可以直接在 mmrotate 或其基于 mmdet 的代码库中实现钩子,并通过仅在训练中修改配置来使用钩子。 这里我们举一个例子:在 mmrotate 中创建一个新的钩子并在训练中使用它。
from mmcv.runner import HOOKS, Hook
@HOOKS.register_module()
class MyHook(Hook):
def __init__(self, a, b):
pass
def before_run(self, runner):
pass
def after_run(self, runner):
pass
def before_epoch(self, runner):
pass
def after_epoch(self, runner):
pass
def before_iter(self, runner):
pass
def after_iter(self, runner):
pass
用户需要根据钩子的功能指定钩子在训练各阶段中( before_run
, after_run
, before_epoch
, after_epoch
, before_iter
, after_iter
)做什么。
2. 注册新的钩子(hook)¶
接下来我们需要导入 MyHook
。如果文件的路径是 mmrotate/core/utils/my_hook.py
,有两种方式导入:
修改
mmrotate/core/utils/__init__.py
文件来导入新定义的模块需要在
mmrotate/core/utils/__init__.py
导入,注册表才会发现并添加该模块:
from .my_hook import MyHook
在配置文件中使用
custom_imports
来手动导入
custom_imports = dict(imports=['mmrotate.core.utils.my_hook'], allow_failed_imports=False)
3. 修改配置¶
custom_hooks = [
dict(type='MyHook', a=a_value, b=b_value)
]
您也可以通过配置键值 priority
为 'NORMAL'
或 'HIGHEST'
来设置钩子的优先级:
custom_hooks = [
dict(type='MyHook', a=a_value, b=b_value, priority='NORMAL')
]
默认地,钩子的优先级在注册时被设置为 NORMAL
。
使用 MMCV 中实现的钩子 (hooks)¶
如果钩子已经在 MMCV 里实现了,您可以直接修改配置文件来使用钩子。
4. 示例: NumClassCheckHook
¶
我们实现了一个自定义的钩子 NumClassCheckHook ,用来检验 head 中的 num_classes
是否与 dataset
中的 CLASSSES
长度匹配。
我们在 default_runtime.py 中对其进行设置。
custom_hooks = [dict(type='NumClassCheckHook')]
修改默认运行挂钩¶
有一些常见的钩子并不通过 custom_hooks
注册,这些钩子包括:
log_config
checkpoint_config
evaluation
lr_config
optimizer_config
momentum_config
这些钩子中,只有记录器钩子(logger hook)是 VERY_LOW
优先级,其他钩子的优先级为 NORMAL
。
前面提到的教程已经介绍了如何修改 optimizer_config
, momentum_config
以及 lr_config
。
这里我们介绍一下如何处理 log_config
, checkpoint_config
以及 evaluation
。
Checkpoint config¶
MMCV runner 将使用 checkpoint_config
来初始化 CheckpointHook
。
checkpoint_config = dict(interval=1)
用户可以设置 max_keep_ckpts
来仅保存一小部分检查点(checkpoint)或者通过设置 save_optimizer
来决定是否保存优化器的状态字典 (state dict of optimizer)。更多使用参数的细节请参考 这里。
How to¶
Migration¶
基准和模型库 (待更新)¶
Rotated RetinaNet-OBB/HBB ( ICCV’2017)
Rotated FasterRCNN-OBB ( TPAMI’2017)
Rotated RepPoints-OBB ( ICCV’2019)
Rotated FCOS (ICCV’2019)
RoI Transformer (CVPR’2019)
Gliding Vertex (TPAMI’2020)
Rotated ATSS-OBB (CVPR’2020)
CSL (ECCV’2020)
R3Det (AAAI’2021)
S2A-Net (TGRS’2021)
ReDet (CVPR’2021)
Beyond Bounding-Box (CVPR’2021)
Oriented R-CNN (ICCV’2021)
GWD (ICML’2021)
KLD (NeurIPS’2021)
SASM (AAAI’2022)
KFIoU (arXiv)
H2RBox (arXiv)
DOTA v1.0 数据集上的结果¶
骨干网络 | mAP | 角度编码方式 | 训练策略 | 显存占用 (GB) | 推理时间 (fps) | 增强方法 | 批量大小 | 配置 | 下载 |
---|---|---|---|---|---|---|---|---|---|
ResNet50 (1024,1024,200) | 59.44 | oc | 1x | 3.45 | 15.6 | - | 2 | rotated-reppoints-qbox_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 64.55 | oc | 1x | 3.38 | 15.7 | - | 2 | rotated-retinanet-hbox-oc_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 65.59 | oc | 1x | 3.12 | 18.5 | - | 2 | rotated_atss_hbb_r50_fpn_1x_dota_oc | model | log |
ResNet50 (1024,1024,200) | 66.45 | oc | 1x | 3.53 | 15.3 | - | 2 | sasm-reppoints-qbox_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 68.42 | le90 | 1x | 3.38 | 16.9 | - | 2 | rotated-retinanet-rbox-le90_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 68.79 | le90 | 1x | 2.36 | 22.4 | - | 2 | rotated-retinanet-rbox-le90_r50_fpn_amp-1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.49 | le135 | 1x | 4.05 | 8.6 | - | 2 | g_reppoints_r50_fpn_1x_dota_le135 | model | log |
ResNet50 (1024,1024,200) | 69.51 | le90 | 1x | 4.40 | 24.0 | - | 2 | rotated-retinanet-rbox-le90_r50_fpn_csl-gaussian_amp-1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.55 | oc | 1x | 3.39 | 15.5 | - | 2 | rotated-retinanet-hbox-oc_r50_fpn_gwd_1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.60 | le90 | 1x | 3.38 | 15.1 | - | 2 | rotated-retinanet-hbox-le90_r50_fpn_kfiou_1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.63 | le135 | 1x | 3.45 | 16.1 | - | 2 | cfa-qbox_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.76 | oc | 1x | 3.39 | 15.6 | - | 2 | rotated-retinanet-hbox-oc_r50_fpn_kfiou_1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.77 | le135 | 1x | 3.38 | 15.3 | - | 2 | rotated-retinanet-hbox-le135_r50_fpn_kfiou_1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.79 | le135 | 1x | 3.38 | 17.2 | - | 2 | rotated-retinanet-rbox-le135_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.80 | oc | 1x | 3.54 | 12.4 | - | 2 | r3det-oc_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 69.94 | oc | 1x | 3.39 | 15.6 | - | 2 | rotated-retinanet-hbox-oc_r50_fpn_kld_1x_dota | model | log |
ResNet50 (1024,1024,200) | 70.18 | oc | 1x | 3.23 | 15.6 | - | 2 | r3det-tiny-oc_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 70.64 | le90 | 1x | 3.12 | 18.2 | - | 2 | rotated_atss_obb_r50_fpn_1x_dota_le90 | model | log |
ResNet50 (1024,1024,200) | 70.70 | le90 | 1x | 4.18 | - | 2 | rotated-fcos-hbox-le90_r50_fpn_1x_dota | model | log | |
ResNet50 (1024,1024,200) | 71.28 | le90 | 1x | 4.18 | - | 2 | rotated-fcos-le90_r50_fpn_1x_dota | model | log | |
ResNet50 (1024,1024,200) | 71.76 | le90 | 1x | 4.23 | - | 2 | rotated-fcos-hbox-le90_r50_fpn_csl-gaussian_1x_dota | model | log | |
ResNet50 (1024,1024,200) | 71.83 | oc | 1x | 3.54 | 12.4 | - | 2 | r3det-oc_r50_fpn_kld-stable_1x_dota | model | log |
ResNet50 (1024,1024,200) | 71.89 | le90 | 1x | 4.18 | - | 2 | rotated-fcos-le90_r50_fpn_kld_1x_dota | model | log | |
ResNet50 (1024,1024,200) | 71.94 | le135 | 1x | 3.45 | 16.1 | - | 2 | oriented-reppoints-qbox_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 72.29 | le135 | 1x | 3.19 | 18.8 | - | 2 | rotated_atss_obb_r50_fpn_1x_dota_le135 | model | log |
ResNet50 (1024,1024,200) | 72.68 | oc | 1x | 3.62 | 12.2 | - | 2 | r3det-oc_r50_fpn_kfiou-ln_1x_dota | model | log |
ResNet50 (1024,1024,200) | 72.76 | oc | 1x | 3.44 | 14.0 | - | 2 | r3det-tiny-oc_r50_fpn_kld_1x_dota | model | log |
ResNet50 (1024,1024,200) | 73.23 | le90 | 1x | 8.45 | 16.4 | - | 2 | gliding-vertex-rbox_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 73.40 | le90 | 1x | 8.46 | 16.5 | - | 2 | rotated-faster-rcnn-le90_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 73.45 | oc | 40e | 3.45 | 16.1 | - | 2 | cfa-qbox_r50_fpn_40e_dota | model | log |
ResNet50 (1024,1024,200) | 73.91 | le135 | 1x | 3.14 | 15.5 | - | 2 | s2anet-le135_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 74.19 | le135 | 1x | 2.17 | 17.4 | - | 2 | s2anet-le135_r50_fpn_amp-1x_dota | model | log |
ResNet50 (1024,1024,200) | 75.63 | le90 | 1x | 7.37 | 21.2 | - | 2 | oriented-rcnn-le90_r50_fpn_amp-1x_dota | model | log |
ResNet50 (1024,1024,200) | 75.69 | le90 | 1x | 8.46 | 16.2 | - | 2 | oriented-rcnn-le90_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,200) | 75.75 | le90 | 1x | 7.56 | 19.3 | - | 2 | roi-trans-le90_r50_fpn_amp-1x_dota | model | log |
ReResNet50 (1024,1024,200) | 75.99 | le90 | 1x | 7.71 | 13.3 | - | 2 | redet-le90_re50_refpn_amp-1x_dota | model | log |
ResNet50 (1024,1024,200) | 76.08 | le90 | 1x | 8.67 | 14.4 | - | 2 | roi-trans-le90_r50_fpn_1x_dota | model | log |
ResNet50 (1024,1024,500) | 76.50 | le90 | 1x | 17.5 | MS+RR | 2 | rotated-retinanet-rbox-le90_r50_fpn_rr-1x_dota-ms | model | log | |
ReResNet50 (1024,1024,200) | 76.68 | le90 | 1x | 9.32 | 10.9 | - | 2 | redet-le90_re50_refpn_1x_dota | model | log |
Swin-tiny (1024,1024,200) | 77.51 | le90 | 1x | 10.9 | - | 2 | roi-trans-le90_swin-tiny_fpn_1x_dota | model | log | |
ResNet50 (1024,1024,500) | 79.66 | le90 | 1x | 14.4 | MS+RR | 2 | roi_trans_r50_fpn_1x_dota_ms_rr_le90 | model | log | |
ReResNet50 (1024,1024,500) | 79.87 | le90 | 1x | 10.9 | MS+RR | 2 | redet-le90_re50_refpn_rr-1x_dota-ms | model | log | |
ResNet50 (1024,1024,200) | 68.75 | le90 | 1x | 6.25 | - | 2 | h2rbox-le90_r50_fpn_adamw-1x_dota | model | log |
MS
表示多尺度图像增强。RR
表示随机旋转增强。
上述模型都是使用 1 * 1080ti/2080ti 训练得到的,并且在 1 * 2080ti 上进行推理测试。
贡献指南(待更新)¶
常见问题解答 (待更新)¶
我们在这里列出了使用时的一些常见问题及其相应的解决方案。 如果您发现有一些问题被遗漏,请随时提 PR 丰富这个列表。如果您无法在此获得帮助,请使用 issue模板 创建问题,但是请在模板中填写所有必填信息,这有助于我们更快定位问题。
MMCV 安装相关¶
MMCV 与 MMDetection 的兼容问题: “ConvWS is already registered in conv layer”; “AssertionError: MMCV==xxx is used but incompatible. Please install mmcv>=xxx, <=xxx.”
MMRotate 和 MMCV, MMDet 版本兼容性如下所示,需要安装正确的版本以避免安装出现问题。
MMRotate | MMEngine | MMCV | MMDetection |
---|---|---|---|
dev-1.x | 0.6.0 \<= mmengine \< 1.0.0 | 2.0.0rc4 \<= mmcv \< 2.1.0 | 3.0.0rc6 \<= mmdet \< 3.1.0 |
1.0.0rc1 | 0.1.0 \<= mmengine \< 1.0.0 | 2.0.0rc2 \<= mmcv \< 2.1.0 | 3.0.0rc5 \<= mmdet \< 3.1.0 |
1.0.0rc0 | 0.1.0 \<= mmengine \< 1.0.0 | 2.0.0rc2 \<= mmcv \< 2.1.0 | 3.0.0rc2 \<= mmdet \< 3.1.0 |
注意:
如果你希望安装 mmrotate-v0.x, MMRotate 和 MMCV 版本兼容表可以在 这里 找到,请选择合适的版本避免安装错误。
在 MMCV-v2.x 中,
mmcv-full
改名为mmcv
,如果你想安装不包含 CUDA 算子的版本,可以选择安装 MMCV 精简版mmcv-lite
。
“No module named ‘mmcv.ops’”; “No module named ‘mmcv._ext’”.
原因是安装了
mmcv-lite
而不是mmcv
。pip uninstall mmcv-lite
卸载安装的mmcv-lite
安装
mmcv
根据 安装说明。
PyTorch/CUDA 环境相关¶
“RTX 30 series card fails when building MMCV or MMDet”
常见报错信息为
nvcc fatal: Unsupported gpu architecture 'compute_86'
意思是你的编译器应该为 sm_86 进行优化,例如, 英伟达 30 系列的显卡,但这样的优化 CUDA toolkit 11.0 并不支持。 此解决方案通过添加MMCV_WITH_OPS=1 MMCV_CUDA_ARGS='-gencode=arch=compute_80,code=sm_80' pip install -e .
来修改编译标志,这告诉编译器nvcc
为 sm_80 进行优化,例如 Nvidia A100,尽管 A100 不同于 30 系列的显卡,但他们使用相似的图灵架构。这种解决方案可能会丧失一些性能但的确有效。PyTorch 开发者已经在 pytorch/pytorch#47585 更新了 PyTorch 默认的编译标志,所以使用 PyTorch-nightly 可能也能解决这个问题,但是我们对此并没有验证这种方式是否有效。
“invalid device function” or “no kernel image is available for execution”.
检查您的 cuda 运行时版本(一般在
/usr/local/
)、指令nvcc --version
显示的版本以及conda list cudatoolkit
指令显式的版本是否匹配。通过运行
python mmdet/utils/collect_env.py
来检查是否为当前的GPU架构编译了正确的 PyTorch、torchvision 和 MMCV,你可能需要设置TORCH_CUDA_ARCH_LIST
来重新安装 MMCV。可以参考 GPU 架构表,即通过运行TORCH_CUDA_ARCH_LIST=7.0 pip install mmcv-full
为 Volta GPU 编译 MMCV。这种架构不匹配的问题一般会出现在使用一些旧型号的 GPU 时候,例如,Tesla K80。检查运行环境是否与 mmcv/mmdet 编译时相同,例如,您可能使用 CUDA 10.0 编译 MMCV,但在 CUDA 9.0 环境中运行它。
“undefined symbol” or “cannot open xxx.so”.
如果这些 symbols 属于 CUDA/C++ (例如,libcudart.so 或者 GLIBCXX),检查 CUDA/GCC 运行时环境是否与编译 MMCV 的一致。例如使用
python mmdet/utils/collect_env.py
检查"MMCV Compiler"
/"MMCV CUDA Compiler"
是否和"GCC"
/"CUDA_HOME"
一致。如果这些 symbols 属于 PyTorch,(例如,symbols containing caffe、aten 和 TH), 检查当前 PyTorch 版本是否与编译 MMCV 的版本一致。
运行
python mmdet/utils/collect_env.py
检查 PyTorch、torchvision、MMCV 等的编译环境与运行环境一致。
“setuptools.sandbox.UnpickleableException: DistutilsSetupError(“each element of ‘ext_modules’ option must be an Extension instance or 2-tuple”)”
如果你在使用 miniconda 而不是 anaconda,检查是否正确的安装了 Cython 如 #3379。您需要先手动安装 Cpython 然后运命令
pip install -r requirements.txt
。检查环境中的
setuptools
、Cython
和PyTorch
相互之间版本是否匹配。
“Segmentation fault”.
检查 GCC 的版本并使用 GCC 5.4,通常是因为 PyTorch 版本与 GCC 版本不匹配 (例如,对于 Pytorch GCC < 4.9),我们推荐用户使用 GCC 5.4,我们也不推荐使用 GCC 5.5, 因为有反馈 GCC 5.5 会导致 “segmentation fault” 并且切换到 GCC 5.4 就可以解决问题。
检查是是否 PyTorch 被正确的安装并可以使用 CUDA 算子,例如在终端中键入如下的指令。
python -c 'import torch; print(torch.cuda.is_available())'
并判断是否是否返回 True。
如果
torch
的安装是正确的,检查是否正确编译了 MMCV。python -c 'import mmcv; import mmcv.ops'
如果 MMCV 被正确的安装了,那么上面的两条指令不会有问题。
如果 MMCV 与 PyTorch 都被正确安装了,则使用
ipdb
、pdb
设置断点,直接查找哪一部分的代码导致了segmentation fault
。
E2CNN¶
“ImportError: cannot import name ‘container_bacs’ from ‘torch._six’”
这是因为
container_abcs
在 PyTorch 1.9 之后被移除.将文件
python3.7/site-packages/e2cnn/nn/modules/module_list.py
中的from torch.six import container_abcs
替换成
TORCH_MAJOR = int(torch.__version__.split('.')[0]) TORCH_MINOR = int(torch.__version__.split('.')[1]) if TORCH_MAJOR ==1 and TORCH_MINOR < 8: from torch.six import container_abcs else: import collections.abs as container_abcs
或者降低 Pytorch 的版本。
Training 相关¶
“Loss goes Nan”
检查数据的标注是否正常,长或宽为 0 的框可能会导致回归 loss 变为 nan,一些小尺寸(宽度或高度小于 1)的框在数据增强(例如,instaboost)后也会导致此问题。因此,可以检查标注并过滤掉那些特别小甚至面积为 0 的框,并关闭一些可能会导致 0 面积框出现数据增强。
降低学习率:由于某些原因,例如 batch size 大小的变化,导致当前学习率可能太大。您可以降低为可以稳定训练模型的值。
延长 warm up 的时间:一些模型在训练初始时对学习率很敏感,您可以把
warmup_iters
从 500 更改为 1000 或 2000。添加 gradient clipping: 一些模型需要梯度裁剪来稳定训练过程。默认的
grad_clip
是None
,你可以在 config 设置optimizer_config=dict(_delete_=True, grad_clip=dict(max_norm=35, norm_type=2))
。 如果你的 config 没有继承任何包含optimizer_config=dict(grad_clip=None)
,你可以直接设置optimizer_config=dict(grad_clip=dict(max_norm=35, norm_type=2))
。
“GPU out of memory”
存在大量 ground truth boxes 或者大量 anchor 的场景,可能在 assigner 会 OOM。您可以在 assigner 的配置中设置
gpu_assign_thr=N
,这样当超过 N 个 GT boxes 时,assigner 会通过 CPU 计算 IoU。在 backbone 中设置
with_cp=True
。这使用 PyTorch 中的sublinear strategy
来降低 backbone 占用的 GPU 显存。通过在配置文件中设置
fp16 = dict(loss_scale='dynamic')
来尝试混合精度训练。
“RuntimeError: Expected to have finished reduction in the prior iteration before starting a new one”
错误表明,您的模块有没用于产生损失的参数,这种现象可能是由于在 DDP 模式下运行代码中的不同分支造成的。
您可以在配置中设置
find_unused_parameters = True
来解决上述问题,或者手动查找那些未使用的参数。
Evaluation 相关¶
使用 COCO Dataset 的测评接口时,测评结果中 AP 或者 AR = -1。
根据 COCO 数据集的定义,一张图像中的中等物体与小物体面积的阈值分别为 9216(96*96)与 1024(32*32)。
如果在某个区间没有物体即 GT,AP 与 AR 将被设置为 -1。