Commit 26b83c4a authored by dengjb's avatar dengjb
Browse files

update codes

parent 2f6baaee
Pipeline #1045 failed with stages
in 0 seconds
.header-logo {
background-image: url("../image/mmdet-logo.png");
background-size: 156px 40px;
height: 40px;
width: 156px;
}
# 默认约定
如果你想把 MMDetection 修改为自己的项目,请遵循下面的约定。
## 关于图片 shape 顺序的说明
在OpenMMLab 2.0中, 为了与 OpenCV 的输入参数相一致,图片处理 pipeline 中关于图像 shape 的输入参数总是以 `(width, height)` 的顺序排列。
相反,为了计算方便,经过 pipeline 和 model 的字段的顺序是 `(height, width)`。具体来说在每个数据 pipeline 处理的结果中,字段和它们的值含义如下:
- img_shape: (height, width)
- ori_shape: (height, width)
- pad_shape: (height, width)
- batch_input_shape: (height, width)
`Mosaic` 为例,其初始化参数如下所示:
```python
@TRANSFORMS.register_module()
class Mosaic(BaseTransform):
def __init__(self,
img_scale: Tuple[int, int] = (640, 640),
center_ratio_range: Tuple[float, float] = (0.5, 1.5),
bbox_clip_border: bool = True,
pad_val: float = 114.0,
prob: float = 1.0) -> None:
...
# img_scale 顺序应该是 (width, height)
self.img_scale = img_scale
def transform(self, results: dict) -> dict:
...
results['img'] = mosaic_img
# (height, width)
results['img_shape'] = mosaic_img.shape[:2]
```
## 损失
在 MMDetection 中,`model(**data)` 的返回值是一个字典,包含着所有的损失和评价指标,他们将会由 `model(**data)` 返回。
例如,在 bbox head 中,
```python
class BBoxHead(nn.Module):
...
def loss(self, ...):
losses = dict()
# 分类损失
losses['loss_cls'] = self.loss_cls(...)
# 分类准确率
losses['acc'] = accuracy(...)
# 边界框损失
losses['loss_bbox'] = self.loss_bbox(...)
return losses
```
`'bbox_head.loss()'` 在模型 forward 阶段会被调用。返回的字典中包含了 `'loss_bbox'`,`'loss_cls'`,`'acc'`。只有 `'loss_bbox'`, `'loss_cls'` 会被用于反向传播,`'acc'` 只会被作为评价指标来监控训练过程。
我们默认,只有那些键的名称中包含 `'loss'` 的值会被用于反向传播。这个行为可以通过修改 `BaseDetector.train_step()` 来改变。
## 空 proposals
在 MMDetection 中,我们为两阶段方法中空 proposals 的情况增加了特殊处理和单元测试。我们同时需要处理整个 batch 和单一图片中空 proposals 的情况。例如,在 CascadeRoIHead 中,
```python
# 简单的测试
...
# 在整个 batch中 都没有 proposals
if rois.shape[0] == 0:
bbox_results = [[
np.zeros((0, 5), dtype=np.float32)
for _ in range(self.bbox_head[-1].num_classes)
]] * num_imgs
if self.with_mask:
mask_classes = self.mask_head[-1].num_classes
segm_results = [[[] for _ in range(mask_classes)]
for _ in range(num_imgs)]
results = list(zip(bbox_results, segm_results))
else:
results = bbox_results
return results
...
# 在单张图片中没有 proposals
for i in range(self.num_stages):
...
if i < self.num_stages - 1:
for j in range(num_imgs):
# 处理空 proposals
if rois[j].shape[0] > 0:
bbox_label = cls_score[j][:, :-1].argmax(dim=1)
refine_roi = self.bbox_head[i].regress_by_class(
rois[j], bbox_label[j], bbox_pred[j], img_metas[j])
refine_roi_list.append(refine_roi)
```
如果你有自定义的 `RoIHead`, 你可以参考上面的方法来处理空 proposals 的情况。
## 全景分割数据集
在 MMDetection 中,我们支持了 COCO 全景分割数据集 `CocoPanopticDataset`。对于它的实现,我们在这里声明一些默认约定。
1. 在 mmdet\<=2.16.0 时,语义分割标注中的前景和背景标签范围与 MMDetection 中的默认规定有所不同。标签 `0` 代表 `VOID` 标签。
从 mmdet=2.17.0 开始,为了和框的类别标注保持一致,语义分割标注的类别标签也改为从 `0` 开始,标签 `255` 代表 `VOID` 类。
为了达成这一目标,我们在流程 `Pad` 里支持了设置 `seg` 的填充值的功能。
2. 在评估中,全景分割结果必须是一个与原图大小相同的图。结果图中每个像素的值有如此形式:`instance_id * INSTANCE_OFFSET + category_id`
# 自定义数据集
## 支持新的数据格式
为了支持新的数据格式,可以选择将数据转换成现成的格式(COCO 或者 PASCAL)或将其转换成中间格式。当然也可以选择以离线的形式(在训练之前使用脚本转换)或者在线的形式(实现一个新的 dataset 在训练中进行转换)来转换数据。
在 MMDetection 中,建议将数据转换成 COCO 格式并以离线的方式进行,因此在完成数据转换后只需修改配置文件中的标注数据的路径和类别即可。
### 将新的数据格式转换为现有的数据格式
最简单的方法就是将你的数据集转换成现有的数据格式(COCO 或者 PASCAL VOC)
COCO 格式的 JSON 标注文件有如下必要的字段:
```python
'images': [
{
'file_name': 'COCO_val2014_000000001268.jpg',
'height': 427,
'width': 640,
'id': 1268
},
...
],
'annotations': [
{
'segmentation': [[192.81,
247.09,
...
219.03,
249.06]], # 如果有 mask 标签且为多边形 XY 点坐标格式,则需要保证至少包括 3 个点坐标,否则为无效多边形
'area': 1035.749,
'iscrowd': 0,
'image_id': 1268,
'bbox': [192.81, 224.8, 74.73, 33.43],
'category_id': 16,
'id': 42986
},
...
],
'categories': [
{'id': 0, 'name': 'car'},
]
```
在 JSON 文件中有三个必要的键:
- `images`: 包含多个图片以及它们的信息的数组,例如 `file_name``height``width``id`
- `annotations`: 包含多个实例标注信息的数组。
- `categories`: 包含多个类别名字和 ID 的数组。
在数据预处理之后,使用现有的数据格式来训练自定义的新数据集有如下两步(以 COCO 为例):
1. 为自定义数据集修改配置文件。
2. 检查自定义数据集的标注。
这里我们举一个例子来展示上面的两个步骤,这个例子使用包括 5 个类别的 COCO 格式的数据集来训练一个现有的 Cascade Mask R-CNN R50-FPN 检测器
#### 1. 为自定义数据集修改配置文件
配置文件的修改涉及两个方面:
1. `dataloaer` 部分。需要在 `train_dataloader.dataset``val_dataloader.dataset``test_dataloader.dataset` 中添加 `metainfo=dict(classes=classes)`, 其中 classes 必须是 tuple 类型。
2. `model` 部分中的 `num_classes`。需要将默认值(COCO 数据集中为 80)修改为自定义数据集中的类别数。
`configs/my_custom_config.py` 内容如下:
```python
# 新的配置来自基础的配置以更好地说明需要修改的地方
_base_ = './cascade_mask_rcnn_r50_fpn_1x_coco.py'
# 1. 数据集设定
dataset_type = 'CocoDataset'
classes = ('a', 'b', 'c', 'd', 'e')
data_root='path/to/your/'
train_dataloader = dict(
batch_size=2,
num_workers=2,
dataset=dict(
type=dataset_type,
# 将类别名字添加至 `metainfo` 字段中
metainfo=dict(classes=classes),
data_root=data_root,
ann_file='train/annotation_data',
data_prefix=dict(img='train/image_data')
)
)
val_dataloader = dict(
batch_size=1,
num_workers=2,
dataset=dict(
type=dataset_type,
test_mode=True,
# 将类别名字添加至 `metainfo` 字段中
metainfo=dict(classes=classes),
data_root=data_root,
ann_file='val/annotation_data',
data_prefix=dict(img='val/image_data')
)
test_dataloader = dict(
batch_size=1,
num_workers=2,
dataset=dict(
type=dataset_type,
test_mode=True,
# 将类别名字添加至 `metainfo` 字段中
metainfo=dict(classes=classes),
data_root=data_root,
ann_file='test/annotation_data',
data_prefix=dict(img='test/image_data')
)
)
# 2. 模型设置
# 将所有的 `num_classes` 默认值修改为 5(原来为80)
model = dict(
roi_head=dict(
bbox_head=[
dict(
type='Shared2FCBBoxHead',
# 将所有的 `num_classes` 默认值修改为 5(原来为 80)
num_classes=5),
dict(
type='Shared2FCBBoxHead',
# 将所有的 `num_classes` 默认值修改为 5(原来为 80)
num_classes=5),
dict(
type='Shared2FCBBoxHead',
# 将所有的 `num_classes` 默认值修改为 5(原来为 80)
num_classes=5)],
# 将所有的 `num_classes` 默认值修改为 5(原来为 80)
mask_head=dict(num_classes=5)))
```
#### 2. 检查自定义数据集的标注
假设你自己的数据集是 COCO 格式,那么需要保证数据的标注没有问题:
1. 标注文件中 `categories` 的长度要与配置中的 `classes` 元组长度相匹配,它们都表示有几类。(如例子中有 5 个类别)
2. 配置文件中 `classes` 字段应与标注文件里 `categories` 下的 `name` 有相同的元素且顺序一致。MMDetection 会自动将 `categories` 中不连续的 `id` 映射成连续的索引,因此 `categories` 下的 `name`的字符串顺序会影响标签的索引。同时,配置文件中的 `classes` 的字符串顺序也会影响到预测框可视化时的标签。
3. `annotations` 中的 `category_id` 必须是有效的值。比如所有 `category_id` 的值都应该属于 `categories` 中的 `id`
下面是一个有效标注的例子:
```python
'annotations': [
{
'segmentation': [[192.81,
247.09,
...
219.03,
249.06]], # 如果有 mask 标签。
'area': 1035.749,
'iscrowd': 0,
'image_id': 1268,
'bbox': [192.81, 224.8, 74.73, 33.43],
'category_id': 16,
'id': 42986
},
...
],
# MMDetection 会自动将 `categories` 中不连续的 `id` 映射成连续的索引。
'categories': [
{'id': 1, 'name': 'a'}, {'id': 3, 'name': 'b'}, {'id': 4, 'name': 'c'}, {'id': 16, 'name': 'd'}, {'id': 17, 'name': 'e'},
]
```
我们使用这种方式来支持 CityScapes 数据集。脚本在 [cityscapes.py](https://github.com/open-mmlab/mmdetection/blob/main/tools/dataset_converters/cityscapes.py) 并且我们提供了微调的 [configs](https://github.com/open-mmlab/mmdetection/blob/main/configs/cityscapes).
**注意**
1. 对于实例分割数据集, **MMDetection 目前只支持评估 COCO 格式的 mask AP**.
2. 推荐训练之前进行离线转换,这样就可以继续使用 `CocoDataset` 且只需修改标注文件的路径以及训练的种类。
### 调整新的数据格式为中间格式
如果不想将标注格式转换为 COCO 或者 PASCAL 格式也是可行的。实际上,我们在 MMEngine 的 [BaseDataset](https://github.com/open-mmlab/mmengine/blob/main/mmengine/dataset/base_dataset.py#L116) 中定义了一种简单的标注格式并且与所有现有的数据格式兼容,也能进行离线或者在线转换。
数据集的标注必须为 `json``yaml``yml``pickle``pkl` 格式;标注文件中存储的字典必须包含 `metainfo``data_list` 两个字段。其中 `metainfo` 是一个字典,里面包含数据集的元信息,例如类别信息;`data_list` 是一个列表,列表中每个元素是一个字典,该字典定义了一个原始数据(raw data),每个原始数据包含一个或若干个训练/测试样本。
以下是一个 JSON 标注文件的例子:
```json
{
'metainfo':
{
'classes': ('person', 'bicycle', 'car', 'motorcycle'),
...
},
'data_list':
[
{
"img_path": "xxx/xxx_1.jpg",
"height": 604,
"width": 640,
"instances":
[
{
"bbox": [0, 0, 10, 20],
"bbox_label": 1,
"ignore_flag": 0
},
{
"bbox": [10, 10, 110, 120],
"bbox_label": 2,
"ignore_flag": 0
}
]
},
{
"img_path": "xxx/xxx_2.jpg",
"height": 320,
"width": 460,
"instances":
[
{
"bbox": [10, 0, 20, 20],
"bbox_label": 3,
"ignore_flag": 1
}
]
},
...
]
}
```
有些数据集可能会提供如:crowd/difficult/ignored bboxes 标注,那么我们使用 `ignore_flag`来包含它们。
在得到上述标准的数据标注格式后,可以直接在配置中使用 MMDetection 的 [BaseDetDataset](https://github.com/open-mmlab/mmdetection/blob/main/mmdet/datasets/base_det_dataset.py#L13) ,而无需进行转换。
### 自定义数据集例子
假设文本文件中表示的是一种全新的标注格式。边界框的标注信息保存在 `annotation.txt` 中,内容如下:
```
#
000001.jpg
1280 720
2
10 20 40 60 1
20 40 50 60 2
#
000002.jpg
1280 720
3
50 20 40 60 2
20 40 30 45 2
30 40 50 60 3
```
我们可以在 `mmdet/datasets/my_dataset.py` 中创建一个新的 dataset 用以加载数据。
```python
import mmengine
from mmdet.base_det_dataset import BaseDetDataset
from mmdet.registry import DATASETS
@DATASETS.register_module()
class MyDataset(BaseDetDataset):
METAINFO = {
'classes': ('person', 'bicycle', 'car', 'motorcycle'),
'palette': [(220, 20, 60), (119, 11, 32), (0, 0, 142), (0, 0, 230)]
}
def load_data_list(self, ann_file):
ann_list = mmengine.list_from_file(ann_file)
data_infos = []
for i, ann_line in enumerate(ann_list):
if ann_line != '#':
continue
img_shape = ann_list[i + 2].split(' ')
width = int(img_shape[0])
height = int(img_shape[1])
bbox_number = int(ann_list[i + 3])
instances = []
for anns in ann_list[i + 4:i + 4 + bbox_number]:
instance = {}
instance['bbox'] = [float(ann) for ann in anns.split(' ')[:4]]
instance['bbox_label']=int(anns[4])
instances.append(instance)
data_infos.append(
dict(
img_path=ann_list[i + 1],
img_id=i,
width=width,
height=height,
instances=instances
))
return data_infos
```
配置文件中,可以使用 `MyDataset` 进行如下修改
```python
dataset_A_train = dict(
type='MyDataset',
ann_file = 'image_list.txt',
pipeline=train_pipeline
)
```
## 使用 dataset 包装器自定义数据集
MMEngine 也支持非常多的数据集包装器(wrapper)来混合数据集或在训练时修改数据集的分布,其支持如下三种数据集包装:
- `RepeatDataset`:将整个数据集简单地重复。
- `ClassBalancedDataset`:以类别均衡的方式重复数据集。
- `ConcatDataset`:合并数据集。
具体使用方式见 [MMEngine 数据集包装器](#TODO)
## 修改数据集的类别
根据现有数据集的类型,我们可以修改它们的类别名称来训练其标注的子集。
例如,如果只想训练当前数据集中的三个类别,那么就可以修改数据集的 `metainfo` 字典,数据集就会自动屏蔽掉其他类别的真实框。
```python
classes = ('person', 'bicycle', 'car')
train_dataloader = dict(
dataset=dict(
metainfo=dict(classes=classes))
)
val_dataloader = dict(
dataset=dict(
metainfo=dict(classes=classes))
)
test_dataloader = dict(
dataset=dict(
metainfo=dict(classes=classes))
)
```
**注意**
- 在 MMDetection v2.5.0 之前,如果类别为集合时数据集将自动过滤掉不包含 GT 的图片,且没办法通过修改配置将其关闭。这是一种不可取的行为而且会引起混淆,因为当类别不是集合时数据集时,只有在 `filter_empty_gt=True` 以及 `test_mode=False` 的情况下才会过滤掉不包含 GT 的图片。在 MMDetection v2.5.0 之后,我们将图片的过滤以及类别的修改进行解耦,数据集只有在 `filter_cfg=dict(filter_empty_gt=True)``test_mode=False` 的情况下才会过滤掉不包含 GT 的图片,无论类别是否为集合。设置类别只会影响用于训练的标注类别,用户可以自行决定是否过滤不包含 GT 的图片。
- 直接使用 MMEngine 中的 `BaseDataset` 或者 MMDetection 中的 `BaseDetDataset` 时用户不能通过修改配置来过滤不含 GT 的图片,但是可以通过离线的方式来解决。
- 当设置数据集中的 `classes` 时,记得修改 `num_classes`。从 v2.9.0 (PR#4508) 之后,我们实现了 [NumClassCheckHook](https://github.com/open-mmlab/mmdetection/blob/main/mmdet/engine/hooks/num_class_check_hook.py) 来检查类别数是否一致。
## COCO 全景分割数据集
现在我们也支持 COCO Panoptic Dataset,全景注释的格式与 COCO 格式不同,其前景和背景都将存在于注释文件中。COCO Panoptic 格式的注释 JSON 文件具有以下必要的键:
```python
'images': [
{
'file_name': '000000001268.jpg',
'height': 427,
'width': 640,
'id': 1268
},
...
]
'annotations': [
{
'filename': '000000001268.jpg',
'image_id': 1268,
'segments_info': [
{
'id':8345037, # One-to-one correspondence with the id in the annotation map.
'category_id': 51,
'iscrowd': 0,
'bbox': (x1, y1, w, h), # The bbox of the background is the outer rectangle of its mask.
'area': 24315
},
...
]
},
...
]
'categories': [ # including both foreground categories and background categories
{'id': 0, 'name': 'person'},
...
]
```
此外,`seg` 必须设置为全景注释图像的路径。
```python
dataset_type = 'CocoPanopticDataset'
data_root='path/to/your/'
train_dataloader = dict(
dataset=dict(
type=dataset_type,
data_root=data_root,
data_prefix=dict(
img='train/image_data/', seg='train/panoptic/image_annotation_data/')
)
)
val_dataloader = dict(
dataset=dict(
type=dataset_type,
data_root=data_root,
data_prefix=dict(
img='val/image_data/', seg='val/panoptic/image_annotation_data/')
)
)
test_dataloader = dict(
dataset=dict(
type=dataset_type,
data_root=data_root,
data_prefix=dict(
img='test/image_data/', seg='test/panoptic/image_annotation_data/')
)
)
```
# 自定义损失函数
MMDetection 为用户提供了不同的损失函数。但是默认的配置可能无法适应不同的数据和模型,所以用户可能会希望修改某一个损失函数来适应新的情况。
本教程首先详细的解释计算损失的过程然后给出一些关于如何修改每一个步骤的指导。对损失的修改可以被分为微调和加权。
## 一个损失的计算过程
给定输入(包括预测和目标,以及权重),损失函数会把输入的张量映射到最后的损失标量。映射过程可以分为下面五个步骤:
1. 设置采样方法为对正负样本进行采样。
2. 通过损失核函数获取**元素**或者**样本**损失。
3. 通过权重张量来给损失**逐元素**权重。
4. 把损失张量归纳为一个**标量**
5. 用一个**张量**给当前损失一个权重。
## 设置采样方法(步骤 1)
对于一些损失函数,需要采样策略来避免正负样本之间的不平衡。
例如,在RPN head中使用`CrossEntropyLoss`时,我们需要在`train_cfg`中设置`RandomSampler`
```python
train_cfg=dict(
rpn=dict(
sampler=dict(
type='RandomSampler',
num=256,
pos_fraction=0.5,
neg_pos_ub=-1,
add_gt_as_proposals=False))
```
对于其他一些具有正负样本平衡机制的损失,例如 Focal Loss、GHMC 和 QualityFocalLoss,不再需要进行采样。
## 微调损失
微调一个损失主要与步骤 2,4,5 有关,大部分的修改可以在配置文件中指定。这里我们用 [Focal Loss (FL)](https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/losses/focal_loss.py) 作为例子。
下面的代码分别是构建 FL 的方法和它的配置文件,他们是一一对应的。
```python
@LOSSES.register_module()
class FocalLoss(nn.Module):
def __init__(self,
use_sigmoid=True,
gamma=2.0,
alpha=0.25,
reduction='mean',
loss_weight=1.0):
```
```python
loss_cls=dict(
type='FocalLoss',
use_sigmoid=True,
gamma=2.0,
alpha=0.25,
loss_weight=1.0)
```
### 微调超参数(步骤2)
`gamma``beta` 是 Focal Loss 中的两个超参数。如果我们想把 `gamma` 的值设为 1.5,把 `alpha` 的值设为 0.5,我们可以在配置文件中按照如下指定:
```python
loss_cls=dict(
type='FocalLoss',
use_sigmoid=True,
gamma=1.5,
alpha=0.5,
loss_weight=1.0)
```
### 微调归纳方式(步骤4)
Focal Loss 默认的归纳方式是 `mean`。如果我们想把归纳方式从 `mean` 改成 `sum`,我们可以在配置文件中按照如下指定:
```python
loss_cls=dict(
type='FocalLoss',
use_sigmoid=True,
gamma=2.0,
alpha=0.25,
loss_weight=1.0,
reduction='sum')
```
### 微调损失权重(步骤5)
这里的损失权重是一个标量,他用来控制多任务学习中不同损失的重要程度,例如,分类损失和回归损失。如果我们想把分类损失的权重设为 0.5,我们可以在配置文件中如下指定:
```python
loss_cls=dict(
type='FocalLoss',
use_sigmoid=True,
gamma=2.0,
alpha=0.25,
loss_weight=0.5)
```
## 加权损失(步骤3)
加权损失就是我们逐元素修改损失权重。更具体来说,我们给损失张量乘以一个与他有相同形状的权重张量。所以,损失中不同的元素可以被赋予不同的比例,所以这里叫做逐元素。损失的权重在不同模型中变化很大,而且与上下文相关,但是总的来说主要有两种损失权重:分类损失的 `label_weights` 和边界框的 `bbox_weights`。你可以在相应的头中的 `get_target` 方法中找到他们。这里我们使用 [ATSSHead](https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/dense_heads/atss_head.py#L322) 作为一个例子。它继承了 [AnchorHead](https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/dense_heads/anchor_head.py) ,但是我们重写它的
`get_targets` 方法来产生不同的 `label_weights``bbox_weights`
```
class ATSSHead(AnchorHead):
...
def get_targets(self,
anchor_list,
valid_flag_list,
gt_bboxes_list,
img_metas,
gt_bboxes_ignore_list=None,
gt_labels_list=None,
label_channels=1,
unmap_outputs=True):
```
# 自定义模型
我们简单地把模型的各个组件分为五类:
- 主干网络 (backbone):通常是一个用来提取特征图 (feature map) 的全卷积网络 (FCN network),例如:ResNet, MobileNet。
- Neck:主干网络和 Head 之间的连接部分,例如:FPN, PAFPN。
- Head:用于具体任务的组件,例如:边界框预测和掩码预测。
- 区域提取器 (roi extractor):从特征图中提取 RoI 特征,例如:RoI Align。
- 损失 (loss):在 Head 组件中用于计算损失的部分,例如:FocalLoss, L1Loss, GHMLoss.
## 开发新的组件
### 添加一个新的主干网络
这里,我们以 MobileNet 为例来展示如何开发新组件。
#### 1. 定义一个新的主干网络(以 MobileNet 为例)
新建一个文件 `mmdet/models/backbones/mobilenet.py`
```python
import torch.nn as nn
from mmdet.registry import MODELS
@MODELS.register_module()
class MobileNet(nn.Module):
def __init__(self, arg1, arg2):
pass
def forward(self, x): # should return a tuple
pass
```
#### 2. 导入该模块
你可以添加下述代码到 `mmdet/models/backbones/__init__.py`
```python
from .mobilenet import MobileNet
```
或添加:
```python
custom_imports = dict(
imports=['mmdet.models.backbones.mobilenet'],
allow_failed_imports=False)
```
到配置文件以避免原始代码被修改。
#### 3. 在你的配置文件中使用该主干网络
```python
model = dict(
...
backbone=dict(
type='MobileNet',
arg1=xxx,
arg2=xxx),
...
```
### 添加新的 Neck
#### 1. 定义一个 Neck(以 PAFPN 为例)
新建一个文件 `mmdet/models/necks/pafpn.py`
```python
import torch.nn as nn
from mmdet.registry import MODELS
@MODELS.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. 导入该模块
你可以添加下述代码到 `mmdet/models/necks/__init__.py`
```python
from .pafpn import PAFPN
```
或添加:
```python
custom_imports = dict(
imports=['mmdet.models.necks.pafpn'],
allow_failed_imports=False)
```
到配置文件以避免原始代码被修改。
#### 3. 修改配置文件
```python
neck=dict(
type='PAFPN',
in_channels=[256, 512, 1024, 2048],
out_channels=256,
num_outs=5)
```
### 添加新的 Head
我们以 [Double Head R-CNN](https://arxiv.org/abs/1904.06493) 为例来展示如何添加一个新的 Head。
首先,添加一个新的 bbox head 到 `mmdet/models/roi_heads/bbox_heads/double_bbox_head.py`
Double Head R-CNN 在目标检测上实现了一个新的 bbox head。为了实现 bbox head,我们需要使用如下的新模块中三个函数。
```python
from typing import Tuple
import torch.nn as nn
from mmcv.cnn import ConvModule
from mmengine.model import BaseModule, ModuleList
from torch import Tensor
from mmdet.models.backbones.resnet import Bottleneck
from mmdet.registry import MODELS
from mmdet.utils import ConfigType, MultiConfig, OptConfigType, OptMultiConfig
from .bbox_head import BBoxHead
@MODELS.register_module()
class DoubleConvFCBBoxHead(BBoxHead):
r"""Bbox head used in Double-Head R-CNN
.. code-block:: none
/-> cls
/-> shared convs ->
\-> reg
roi features
/-> cls
\-> shared fc ->
\-> reg
""" # noqa: W605
def __init__(self,
num_convs: int = 0,
num_fcs: int = 0,
conv_out_channels: int = 1024,
fc_out_channels: int = 1024,
conv_cfg: OptConfigType = None,
norm_cfg: ConfigType = dict(type='BN'),
init_cfg: MultiConfig = dict(
type='Normal',
override=[
dict(type='Normal', name='fc_cls', std=0.01),
dict(type='Normal', name='fc_reg', std=0.001),
dict(
type='Xavier',
name='fc_branch',
distribution='uniform')
]),
**kwargs) -> None:
kwargs.setdefault('with_avg_pool', True)
super().__init__(init_cfg=init_cfg, **kwargs)
def forward(self, x_cls: Tensor, x_reg: Tensor) -> Tuple[Tensor]:
```
然后,如有必要,实现一个新的 bbox head。我们打算从 `StandardRoIHead` 来继承新的 `DoubleHeadRoIHead`。我们可以发现 `StandardRoIHead` 已经实现了下述函数。
```python
from typing import List, Optional, Tuple
import torch
from torch import Tensor
from mmdet.registry import MODELS, TASK_UTILS
from mmdet.structures import DetDataSample
from mmdet.structures.bbox import bbox2roi
from mmdet.utils import ConfigType, InstanceList
from ..task_modules.samplers import SamplingResult
from ..utils import empty_instances, unpack_gt_instances
from .base_roi_head import BaseRoIHead
@MODELS.register_module()
class StandardRoIHead(BaseRoIHead):
"""Simplest base roi head including one bbox head and one mask head."""
def init_assigner_sampler(self) -> None:
def init_bbox_head(self, bbox_roi_extractor: ConfigType,
bbox_head: ConfigType) -> None:
def init_mask_head(self, mask_roi_extractor: ConfigType,
mask_head: ConfigType) -> None:
def forward(self, x: Tuple[Tensor],
rpn_results_list: InstanceList) -> tuple:
def loss(self, x: Tuple[Tensor], rpn_results_list: InstanceList,
batch_data_samples: List[DetDataSample]) -> dict:
def _bbox_forward(self, x: Tuple[Tensor], rois: Tensor) -> dict:
def bbox_loss(self, x: Tuple[Tensor],
sampling_results: List[SamplingResult]) -> dict:
def mask_loss(self, x: Tuple[Tensor],
sampling_results: List[SamplingResult], bbox_feats: Tensor,
batch_gt_instances: InstanceList) -> dict:
def _mask_forward(self,
x: Tuple[Tensor],
rois: Tensor = None,
pos_inds: Optional[Tensor] = None,
bbox_feats: Optional[Tensor] = None) -> dict:
def predict_bbox(self,
x: Tuple[Tensor],
batch_img_metas: List[dict],
rpn_results_list: InstanceList,
rcnn_test_cfg: ConfigType,
rescale: bool = False) -> InstanceList:
def predict_mask(self,
x: Tuple[Tensor],
batch_img_metas: List[dict],
results_list: InstanceList,
rescale: bool = False) -> InstanceList:
```
Double Head 的修改主要在 bbox_forward 的逻辑中,且它从 `StandardRoIHead` 中继承了其他逻辑。在 `mmdet/models/roi_heads/double_roi_head.py` 中,我们用下述代码实现新的 bbox head:
```python
from typing import Tuple
from torch import Tensor
from mmdet.registry import MODELS
from .standard_roi_head import StandardRoIHead
@MODELS.register_module()
class DoubleHeadRoIHead(StandardRoIHead):
"""RoI head for `Double Head RCNN <https://arxiv.org/abs/1904.06493>`_.
Args:
reg_roi_scale_factor (float): The scale factor to extend the rois
used to extract the regression features.
"""
def __init__(self, reg_roi_scale_factor: float, **kwargs):
super().__init__(**kwargs)
self.reg_roi_scale_factor = reg_roi_scale_factor
def _bbox_forward(self, x: Tuple[Tensor], rois: Tensor) -> dict:
"""Box head forward function used in both training and testing.
Args:
x (tuple[Tensor]): List of multi-level img features.
rois (Tensor): RoIs with the shape (n, 5) where the first
column indicates batch id of each RoI.
Returns:
dict[str, Tensor]: Usually returns a dictionary with keys:
- `cls_score` (Tensor): Classification scores.
- `bbox_pred` (Tensor): Box energies / deltas.
- `bbox_feats` (Tensor): Extract bbox RoI features.
"""
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
```
最终,用户需要把该模块添加到 `mmdet/models/bbox_heads/__init__.py``mmdet/models/roi_heads/__init__.py` 以使相关的注册表可以找到并加载他们。
或者,用户可以添加:
```python
custom_imports=dict(
imports=['mmdet.models.roi_heads.double_roi_head', 'mmdet.models.roi_heads.bbox_heads.double_bbox_head'])
```
到配置文件并实现相同的目的。
Double Head R-CNN 的配置文件如下:
```python
_base_ = '../faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py'
model = dict(
roi_head=dict(
type='DoubleHeadRoIHead',
reg_roi_scale_factor=1.3,
bbox_head=dict(
_delete_=True,
type='DoubleConvFCBBoxHead',
num_convs=4,
num_fcs=2,
in_channels=256,
conv_out_channels=1024,
fc_out_channels=1024,
roi_feat_size=7,
num_classes=80,
bbox_coder=dict(
type='DeltaXYWHBBoxCoder',
target_means=[0., 0., 0., 0.],
target_stds=[0.1, 0.1, 0.2, 0.2]),
reg_class_agnostic=False,
loss_cls=dict(
type='CrossEntropyLoss', use_sigmoid=False, loss_weight=2.0),
loss_bbox=dict(type='SmoothL1Loss', beta=1.0, loss_weight=2.0))))
```
从 MMDetection 2.0 版本起,配置系统支持继承配置以使用户可以专注于修改。
Double Head R-CNN 主要使用了一个新的 `DoubleHeadRoIHead` 和一个新的 `DoubleConvFCBBoxHead`,参数需要根据每个模块的 `__init__` 函数来设置。
### 添加新的损失
假设你想添加一个新的损失 `MyLoss` 用于边界框回归。
为了添加一个新的损失函数,用户需要在 `mmdet/models/losses/my_loss.py` 中实现。
装饰器 `weighted_loss` 可以使损失每个部分加权。
```python
import torch
import torch.nn as nn
from mmdet.registry import LOSSES
from .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
@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
```
然后,用户需要把它加到 `mmdet/models/losses/__init__.py`
```python
from .my_loss import MyLoss, my_loss
```
或者,你可以添加:
```python
custom_imports=dict(
imports=['mmdet.models.losses.my_loss'])
```
到配置文件来实现相同的目的。
如使用,请修改 `loss_xxx` 字段。
因为 MyLoss 是用于回归的,你需要在 Head 中修改 `loss_xxx` 字段。
```python
loss_bbox=dict(type='MyLoss', loss_weight=1.0))
```
# 自定义训练配置
## 自定义优化相关的配置
优化相关的配置现在已全部集成到 `optim_wrapper` 中,通常包含三个域:`optimizer`, `paramwise_cfg``clip_grad`,具体细节见 [OptimWrapper](https://mmengine.readthedocs.io/en/latest/tutorials/optim_wrapper.md)。下面这个例子中,使用了 `AdamW` 作为优化器,主干部分的学习率缩小到原来的十分之一,以及添加了梯度裁剪。
```python
optim_wrapper = dict(
type='OptimWrapper',
# 优化器
optimizer=dict(
type='AdamW',
lr=0.0001,
weight_decay=0.05,
eps=1e-8,
betas=(0.9, 0.999)),
# 参数层面的学习率和正则化设置
paramwise_cfg=dict(
custom_keys={
'backbone': dict(lr_mult=0.1, decay_mult=1.0),
},
norm_decay_mult=0.0),
# 梯度裁剪
clip_grad=dict(max_norm=0.01, norm_type=2))
```
### 自定义 Pytorch 中优化器设置
我们已经支持了 Pytorch 中实现的所有优化器,要使用这些优化器唯一要做就是修改配置文件中的 `optimi_wrapper` 中的 `optimzer` 域。比如,如果想要使用 `ADAM` 作为优化器(可能会导致性能下降),所需要做的修改如下。
```python
optim_wrapper = dict(
type='OptimWrapper',
optimizer=dict(type='Adam', lr=0.0003, weight_decay=0.0001))
```
要修改模型的学习率,用户只需要修改 `optimizer` 中的 `lr` 域。用户可以直接参考 PyToch 的 [API doc](https://pytorch.org/docs/stable/optim.html?highlight=optim#module-torch.optim) 来进行参数的设置。
### 自定义优化器
#### 1. 定义一个新优化器
自定义优化器可以定义的方式如下:
假设你想要添加一个名为 `MyOptimizer` 的优化器,它包含三个参数 `a``b``c`。你需要新建一个名为
`mmdet/engine/optimizers` 的文件夹。然后在文件(比如,`mmdet/engine/optimizers/my_optimizer.py`)实现一个新的优化器。
```python
from mmdet.registry import OPTIMIZERS
from torch.optim import Optimizer
@OPTIMIZERS.register_module()
class MyOptimizer(Optimizer):
def __init__(self, a, b, c)
```
#### 2. 导入自定义的优化器
为了能找到上面的所定义的模块,这个模块必须要先导入到主命名空间中。有两种方式可以实现这一点。
- 修改 `mmdet/engine/optimizers/__init__.py` 来导入模块。
新定义的模块必须导入到 `mmdet/engine/optimizers/__init__.py`,这样注册器才能找到该模块并添加它。
```python
from .my_optimizer import MyOptimizer
```
- 在配置文件使用 `custom_imports` 来手动导入模块。
```python
custom_imports = dict(imports=['mmdet.engine.optimizers.my_optimizer'], allow_failed_imports=False)
```
`mmdet.engine.optimizers.my_optimizer` 模块将在程序开始时导入,之后 `MyOptimizer` 类会被自动注册。注意:应该导入 `MyOptimizer` 所在的文件,即 `mmdet.engine.optimizers.my_optimizer`,而不是 `mmdet.engine.optimizers.my_optimizer.MyOptimizer`
实际上,用户也可以在别的目录结构下来进行导入模块,只要改模块可以在 `PYTHONPATH` 中找到。
#### 3. 在配置文件中指定优化器
接下来,你可以在配置文件中的 `optim_wrapper` 域中的中 `optimizer` 域中设置你实现的优化器 `MyOptimizer`。在配置文件中,优化器在 `optimizer` 域中的配置方式如下:
```python
optim_wrapper = dict(
type='OptimWrapper',
optimizer=dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001))
```
为了使用你的优化器,可以进行如下修改
```python
optim_wrapper = dict(
type='OptimWrapper',
optimizer=dict(type='MyOptimizer', a=a_value, b=b_value, c=c_value))
```
### 自定义优化器包装构造类
一些模型可能存在一些特定参数的优化设置,比如,BN 层的权重衰减。用户可以通过自定义优化器包装构造类来实现这些精细化的参数调整。
```python
from mmengine.optim import DefaultOptiWrapperConstructor
from mmdet.registry import OPTIM_WRAPPER_CONSTRUCTORS
from .my_optimizer import MyOptimizer
@OPTIM_WRAPPER_CONSTRUCTORS.register_module()
class MyOptimizerWrapperConstructor(DefaultOptimWrapperConstructor):
def __init__(self,
optim_wrapper_cfg: dict,
paramwise_cfg: Optional[dict] = None):
def __call__(self, model: nn.Module) -> OptimWrapper:
return optim_wrapper
```
优化器包装构造类的具体实现见[这里](https://github.com/open-mmlab/mmengine/blob/main/mmengine/optim/optimizer/default_constructor.py#L18),用户以它为模板,来实现新的优化器包装构造类。
### 额外的设置
一些没有被优化器实现的技巧(比如,参数层面的学习率设置)应该通过优化器包装构造类来实现或者钩子。我们列出了一些常用的设置用于稳定训练或者加速训练。请随意创建 PR,发布更多设置。
- __使用梯度裁剪来稳定训练__:
一些模型需要进行梯度裁剪来稳定训练过程,例子如下:
```python
optim_wrapper = dict(
_delete_=True, clip_grad=dict(max_norm=35, norm_type=2))
```
如果你的配置已经集成了基础配置(包含了 `optim_wrapper` 的配置),那么你需要添加 `_delete_=True` 来覆盖掉不需要的设置。具体见[配置相关的文档](https://mmdetection.readthedocs.io/en/latest/tutorials/config.html)
- __使用动量调度加速模型收敛__:
我们支持动量调度器根据学习率修改模型的动量,这可以使模型以更快的方式收敛。动量调度器通常与学习率调度器一起使用,例如 [3D 检测](https://github.com/open-mmlab/mmdetection3d/blob/dev-1.x/configs/_base_/schedules/cyclic-20e.py) 中使用以下配置以加速收敛。
更多细节请参考 [CosineAnnealingLR](https://github.com/open-mmlab/mmengine/blob/main/mmengine/optim/scheduler/lr_scheduler.py#L43)[CosineAnnealingMomentum](https://github.com/open-mmlab/mmengine/blob/main/mmengine/optim/scheduler/momentum_scheduler.py#L71) 的具体实现。
```python
param_scheduler = [
# 学习率调度器
# 在前 8 个 epoch, 学习率从 0 增大到 lr * 10
# 在接下来 12 个 epoch, 学习率从 lr * 10 减小到 lr * 1e-4
dict(
type='CosineAnnealingLR',
T_max=8,
eta_min=lr * 10,
begin=0,
end=8,
by_epoch=True,
convert_to_iter_based=True),
dict(
type='CosineAnnealingLR',
T_max=12,
eta_min=lr * 1e-4,
begin=8,
end=20,
by_epoch=True,
convert_to_iter_based=True),
# 动量调度器
# 在前 8 个 epoch, 动量从 0 增大到 0.85 / 0.95
# 在接下来 12 个 epoch, 学习率从 0.85 / 0.95 增大到 1
dict(
type='CosineAnnealingMomentum',
T_max=8,
eta_min=0.85 / 0.95,
begin=0,
end=8,
by_epoch=True,
convert_to_iter_based=True),
dict(
type='CosineAnnealingMomentum',
T_max=12,
eta_min=1,
begin=8,
end=20,
by_epoch=True,
convert_to_iter_based=True)
]
```
## 自定义训练策略
默认情况下,我们使用 1x 的学习率调整策略,这会条用 MMEngine 中的 [MultiStepLR](https://github.com/open-mmlab/mmengine/blob/main/mmengine/optim/scheduler/lr_scheduler.py#L139)
我们支持许多其他学习率调整策略,具体见[这里](https://github.com/open-mmlab/mmengine/blob/main/mmengine/optim/scheduler/lr_scheduler.py),例如 `CosineAnnealingLR``PolyLR` 策略。下面有些例子
- 多项式学习率调整策略:
```python
param_scheduler = [
dict(
type='PolyLR',
power=0.9,
eta_min=1e-4,
begin=0,
end=8,
by_epoch=True)]
```
- 余弦退火学习率调整策略
```python
param_scheduler = [
dict(
type='CosineAnnealingLR',
T_max=8,
eta_min=lr * 1e-5,
begin=0,
end=8,
by_epoch=True)]
```
## 自定义训练循环
默认情况下,在 `train_cfg` 中使用 `EpochBasedTrainLoop`,并且在每个 epoch 训练之后进行验证,如下所示。
```python
train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=12, val_begin=1, val_interval=1)
```
实际上,[`IterBasedTrainLoop`](https://github.com/open-mmlab/mmengine/blob/main/mmengine/runner/loops.py#L183%5D)\[`EpochBasedTrainLoop`\](https:// github.com/open-mmlab/mmengine/blob/main/mmengine/runner/loops.py#L18) 支持动态区间的方式进行验证,见下例。
```python
# 在第 365001 次迭代之前,我们每 5000 次迭代进行一次评估。
# 在第 365000 次迭代后,我们每 368750 次迭代进行一次评估,
# 这意味着我们在训练结束时进行评估。
interval = 5000
max_iters = 368750
dynamic_intervals = [(max_iters // interval * interval + 1, max_iters)]
train_cfg = dict(
type='IterBasedTrainLoop',
max_iters=max_iters,
val_interval=interval,
dynamic_intervals=dynamic_intervals)
```
## 自定义钩子
### 自定义自行实现的钩子
#### 1. 实现一个新的钩子
MMEngine 提供了许多有用的[钩子](https://mmdetection.readthedocs.io/en/latest/tutorials/hooks.html),但在某些情况下用户可能需要实现新的钩子。MMDetection 在 v3.0 中支持自定义钩子。因此,用户可以直接在 mmdet 或其基于 mmdet 的代码库中实现钩子,并通过仅在训练中修改配置来使用钩子。
这里我们给出一个在 mmdet 中创建一个新的钩子并在训练中使用它的例子。
```python
from mmengine.hooks import Hook
from mmdet.registry import HOOKS
@HOOKS.register_module()
class MyHook(Hook):
def __init__(self, a, b):
def before_run(self, runner) -> None:
def after_run(self, runner) -> None:
def before_train(self, runner) -> None:
def after_train(self, runner) -> None:
def before_train_epoch(self, runner) -> None:
def after_train_epoch(self, runner) -> None:
def before_train_iter(self,
runner,
batch_idx: int,
data_batch: DATA_BATCH = None) -> None:
def after_train_iter(self,
runner,
batch_idx: int,
data_batch: DATA_BATCH = None,
outputs: Optional[dict] = None) -> None:
```
根据钩子的功能,用户需要在 `before_run``after_run``before_train``after_train``before_train_epoch``after_train_epoch``before_train_iter``after_train_iter`。还有更多可以插入钩子的点,更多细节请参考 [base hook class](https://github.com/open-mmlab/mmengine/blob/main/mmengine/hooks/hook.py#L9)
#### 2. 注册新钩子
然后我们需要导入 `MyHook`。假设该文件位于 `mmdet/engine/hooks/my_hook.py` 中,有两种方法可以做到这一点:
- 修改 `mmdet/engine/hooks/__init__.py` 以导入它。
新定义的模块应该在 `mmdet/engine/hooks/__init__.py` 中导入,以便注册表找到新模块并添加它:
```python
from .my_hook import MyHook
```
- 在配置中使用 `custom_imports` 手动导入它
```python
custom_imports = dict(imports=['mmdet.engine.hooks.my_hook'], allow_failed_imports=False)
```
#### 3. 修改配置
```python
custom_hooks = [
dict(type='MyHook', a=a_value, b=b_value)
]
```
你还可以通过修改键 `priority` 的值为 `NORMAL``HIGHEST` 来设置挂钩的优先级,如下所示
```python
custom_hooks = [
dict(type='MyHook', a=a_value, b=b_value, priority='NORMAL')
]
```
默认情况下,钩子的优先级在注册期间设置为 `NORMAL`
### 使用 MMDetection 中实现的钩子
如果 MMDetection 中已经实现了该钩子,你可以直接修改配置以使用该钩子,如下所示
#### 例子: `NumClassCheckHook`
我们实现了一个名为 [NumClassCheckHook](https://github.com/open-mmlab/mmdetection/blob/main/mmdet/engine/hooks/num_class_check_hook.py) 的自定义钩子来检查 `num_classes` 是否在 head 中和 `dataset` 中的 `classes` 的长度相匹配。
我们在 [default_runtime.py](https://github.com/open-mmlab/mmdetection/blob/main/configs/_base_/default_runtime.py) 中设置它。
```python
custom_hooks = [dict(type='NumClassCheckHook')]
```
### 修改默认运行时钩子
有一些常见的钩子是通过 `default_hooks` 注册的,它们是
- `IterTimerHook`:记录 “data_time” 用于加载数据和 “time” 用于模型训练步骤的钩子。
- `LoggerHook`:从`Runner`的不同组件收集日志并将它们写入终端、JSON文件、tensorboard和 wandb 等的钩子。
- `ParamSchedulerHook`:更新优化器中一些超参数的钩子,例如学习率和动量。
- `CheckpointHook`:定期保存检查点的钩子。
- `DistSamplerSeedHook`:为采样器和批处理采样器设置种子的钩子。
- `DetVisualizationHook`:用于可视化验证和测试过程预测结果的钩子。
`IterTimerHook``ParamSchedulerHook``DistSamplerSeedHook` 很简单,通常不需要修改,所以这里我们将展示如何使用 `LoggerHook``CheckpointHook``DetVisualizationHook`
#### CheckpointHook
除了定期保存检查点,[`CheckpointHook`](https://github.com/open-mmlab/mmengine/blob/main/mmengine/hooks/checkpoint_hook.py#L19) 提供了其他选项,例如`max_keep_ckpts``save_optimizer ` 等。用户可以设置 `max_keep_ckpts` 只保存少量检查点或通过 `save_optimizer` 决定是否存储优化器的状态字典。参数的更多细节在[这里](https://github.com/open-mmlab/mmengine/blob/main/mmengine/hooks/checkpoint_hook.py#L19)可以找到。
```python
default_hooks = dict(
checkpoint=dict(
type='CheckpointHook',
interval=1,
max_keep_ckpts=3,
save_optimizer=True))
```
#### LoggerHook
`LoggerHook` 可以设置间隔。详细用法可以在 [docstring](https://github.com/open-mmlab/mmengine/blob/main/mmengine/hooks/logger_hook.py#L18) 中找到。
```python
default_hooks = dict(logger=dict(type='LoggerHook', interval=50))
```
#### DetVisualizationHook
`DetVisualizationHook` 使用 `DetLocalVisualizer` 来可视化预测结果,`DetLocalVisualizer` 支持不同的后端,例如 `TensorboardVisBackend``WandbVisBackend` (见 [docstring](https://github.com/open-mmlab/mmengine/blob/main/mmengine/visualization/vis_backend.py) 了解更多细节)。用户可以添加多个后端来进行可视化,如下所示。
```python
default_hooks = dict(
visualization=dict(type='DetVisualizationHook', draw=True))
vis_backends = [dict(type='LocalVisBackend'),
dict(type='TensorboardVisBackend')]
visualizer = dict(
type='DetLocalVisualizer', vis_backends=vis_backends, name='visualizer')
```
# 自定义数据预处理流程
1. 在任意文件里写一个新的流程,例如在 `my_pipeline.py`,它以一个字典作为输入并且输出一个字典:
```python
import random
from mmcv.transforms import BaseTransform
from mmdet.registry import TRANSFORMS
@TRANSFORMS.register_module()
class MyTransform(BaseTransform):
"""Add your transform
Args:
p (float): Probability of shifts. Default 0.5.
"""
def __init__(self, prob=0.5):
self.prob = prob
def transform(self, results):
if random.random() > self.prob:
results['dummy'] = True
return results
```
2. 在配置文件里调用并使用你写的数据处理流程,需要确保你的训练脚本能够正确导入新增模块:
```python
custom_imports = dict(imports=['path.to.my_pipeline'], allow_failed_imports=False)
train_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True),
dict(type='Resize', scale=(1333, 800), keep_ratio=True),
dict(type='RandomFlip', prob=0.5),
dict(type='MyTransform', prob=0.2),
dict(type='PackDetInputs')
]
```
3. 可视化数据增强处理流程的结果
如果想要可视化数据增强处理流程的结果,可以使用 `tools/misc/browse_dataset.py` 直观
地浏览检测数据集(图像和标注信息),或将图像保存到指定目录。
使用方法请参考[可视化文档](../user_guides/visualization.md)
# 执行引擎(待更新)
# 精度评测(待更新)
本教程收集了任何如何使用 MMDetection 进行 xxx 的答案。 如果您遇到有关`如何做`的问题及答案,请随时更新此文档!
## 使用 MMPretrain 的骨干网络
MMDet、MMPretrain、MMSeg 中的模型注册表都继承自 MMEngine 中的根注册表,允许这些存储库直接使用彼此已经实现的模块。 因此用户可以在 MMDetection 中使用来自 MMPretrain 的骨干网络,而无需实现MMPretrain 中已经存在的网络。
### 使用在 MMPretrain 中实现的骨干网络
假设想将 `MobileNetV3-small` 作为 `RetinaNet` 的骨干网络,则配置文件如下。
```python
_base_ = [
'../_base_/models/retinanet_r50_fpn.py',
'../_base_/datasets/coco_detection.py',
'../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py'
]
# please install mmpretrain
# import mmpretrain.models to trigger register_module in mmpretrain
custom_imports = dict(imports=['mmpretrain.models'], allow_failed_imports=False)
pretrained = 'https://download.openmmlab.com/mmclassification/v0/mobilenet_v3/convert/mobilenet_v3_small-8427ecf0.pth'
model = dict(
backbone=dict(
_delete_=True, # 将 _base_ 中关于 backbone 的字段删除
type='mmpretrain.MobileNetV3', # 使用 mmpretrain 中的 MobileNetV3
arch='small',
out_indices=(3, 8, 11), # 修改 out_indices
init_cfg=dict(
type='Pretrained',
checkpoint=pretrained,
prefix='backbone.')), # mmpretrain 中骨干网络的预训练权重含义 prefix='backbone.',为了正常加载权重,需要把这个 prefix 去掉。
# 修改 in_channels
neck=dict(in_channels=[24, 48, 96], start_level=0))
```
### 通过 MMPretrain 使用 TIMM 中实现的骨干网络
由于 MMPretrain 提供了 Py**T**orch **Im**age **M**odels (`timm`) 骨干网络的封装,用户也可以通过 MMPretrain 直接使用 `timm` 中的骨干网络。假设想将 [`EfficientNet-B1`](../../../configs/timm_example/retinanet_timm-efficientnet-b1_fpn_1x_coco.py) 作为 `RetinaNet` 的骨干网络,则配置文件如下。
```python
# https://github.com/open-mmlab/mmdetection/blob/main/configs/timm_example/retinanet_timm_efficientnet_b1_fpn_1x_coco.py
_base_ = [
'../_base_/models/retinanet_r50_fpn.py',
'../_base_/datasets/coco_detection.py',
'../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py'
]
# please install mmpretrain
# import mmpretrain.models to trigger register_module in mmpretrain
custom_imports = dict(imports=['mmpretrain.models'], allow_failed_imports=False)
model = dict(
backbone=dict(
_delete_=True, # 将 _base_ 中关于 backbone 的字段删除
type='mmpretrain.TIMMBackbone', # 使用 mmpretrain 中 timm 骨干网络
model_name='efficientnet_b1',
features_only=True,
pretrained=True,
out_indices=(1, 2, 3, 4)), # 修改 out_indices
neck=dict(in_channels=[24, 40, 112, 320])) # 修改 in_channels
optimizer = dict(type='SGD', lr=0.01, momentum=0.9, weight_decay=0.0001)
```
`type='mmpretrain.TIMMBackbone'` 表示在 MMDetection 中使用 MMPretrain 中的 `TIMMBackbone` 类,并且使用的模型为` EfficientNet-B1`,其中 `mmpretrain` 表示 MMPretrain 库,而 `TIMMBackbone ` 表示 MMPretrain 中实现的 TIMMBackbone 包装器。
关于层次注册器的具体原理可以参考 [MMEngine 文档](https://mmengine.readthedocs.io/zh_cn/latest/tutorials/config.md#跨项目继承配置文件),关于如何使用 MMPretrain 中的其他 backbone,可以参考 [MMPretrain 文档](https://mmpretrain.readthedocs.io/en/latest/user_guides/config.html)
## 使用马赛克数据增强
如果你想在训练中使用 `Mosaic`,那么请确保你同时使用 `MultiImageMixDataset`。以 `Faster R-CNN` 算法为例,你可以通过如下做法实现:
```python
# 直接打开 configs/faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py ,增添如下字段
data_root = 'data/coco/'
dataset_type = 'CocoDataset'
img_scale=(1333, 800)
train_pipeline = [
dict(type='Mosaic', img_scale=img_scale, pad_val=114.0),
dict(
type='RandomAffine',
scaling_ratio_range=(0.1, 2),
border=(-img_scale[0] // 2, -img_scale[1] // 2)), # 图像经过马赛克处理后会放大4倍,所以我们使用仿射变换来恢复图像的大小。
dict(type='RandomFlip', prob=0.5),
dict(type='PackDetInputs'))
]
train_dataset = dict(
_delete_ = True, # 删除不必要的设置
type='MultiImageMixDataset',
dataset=dict(
type=dataset_type,
ann_file=data_root + 'annotations/instances_train2017.json',
img_prefix=data_root + 'train2017/',
pipeline=[
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True)
],
filter_empty_gt=False,
),
pipeline=train_pipeline
)
data = dict(
train=train_dataset
)
```
## 在配置文件中冻结骨干网络后在训练中解冻骨干网络
如果你在配置文件中已经冻结了骨干网络并希望在几个训练周期后解冻它,你可以通过 hook 来实现这个功能。以用 ResNet 为骨干网络的 Faster R-CNN 为例,你可以冻结一个骨干网络的一个层并在配置文件中添加如下 `custom_hooks`:
```python
_base_ = [
'../_base_/models/faster-rcnn_r50_fpn.py',
'../_base_/datasets/coco_detection.py',
'../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py'
]
model = dict(
# freeze one stage of the backbone network.
backbone=dict(frozen_stages=1),
)
custom_hooks = [dict(type="UnfreezeBackboneEpochBasedHook", unfreeze_epoch=1)]
```
同时在 `mmdet/core/hook/unfreeze_backbone_epoch_based_hook.py` 当中书写 `UnfreezeBackboneEpochBasedHook`
```python
from mmengine.model import is_model_wrapper
from mmengine.hooks import Hook
from mmdet.registry import HOOKS
@HOOKS.register_module()
class UnfreezeBackboneEpochBasedHook(Hook):
"""Unfreeze backbone network Hook.
Args:
unfreeze_epoch (int): The epoch unfreezing the backbone network.
"""
def __init__(self, unfreeze_epoch=1):
self.unfreeze_epoch = unfreeze_epoch
def before_train_epoch(self, runner):
# Unfreeze the backbone network.
# Only valid for resnet.
if runner.epoch == self.unfreeze_epoch:
model = runner.model
if is_module_wrapper(model):
model = model.module
backbone = model.backbone
if backbone.frozen_stages >= 0:
if backbone.deep_stem:
backbone.stem.train()
for param in backbone.stem.parameters():
param.requires_grad = True
else:
backbone.norm1.train()
for m in [backbone.conv1, backbone.norm1]:
for param in m.parameters():
param.requires_grad = True
for i in range(1, backbone.frozen_stages + 1):
m = getattr(backbone, f'layer{i}')
m.train()
for param in m.parameters():
param.requires_grad = True
```
## 获得新的骨干网络的通道数
如果你想获得一个新骨干网络的通道数,你可以单独构建这个骨干网络并输入一个伪造的图片来获取每一个阶段的输出。
`ResNet` 为例:
```python
from mmdet.models import ResNet
import torch
self = ResNet(depth=18)
self.eval()
inputs = torch.rand(1, 3, 32, 32)
level_outputs = self.forward(inputs)
for level_out in level_outputs:
print(tuple(level_out.shape))
```
以上脚本的输出为:
```python
(1, 64, 8, 8)
(1, 128, 4, 4)
(1, 256, 2, 2)
(1, 512, 1, 1)
```
用户可以通过将脚本中的 `ResNet(depth=18)` 替换为自己的骨干网络配置来得到新的骨干网络的通道数。
# MMDetection 中训练 Detectron2 的模型
用户可以使用 `Detectron2Wrapper` 从而在 MMDetection 中使用 Detectron2 的模型。
我们提供了 [Faster R-CNN](../../../configs/misc/d2_faster-rcnn_r50-caffe_fpn_ms-90k_coco.py),
[Mask R-CNN](../../../configs/misc/d2_mask-rcnn_r50-caffe_fpn_ms-90k_coco.py)[RetinaNet](../../../configs/misc/d2_retinanet_r50-caffe_fpn_ms-90k_coco.py) 的示例来在 MMDetection 中训练/测试 Detectron2 的模型。
使用过程中需要注意配置文件中算法组件要和 Detectron2 中的相同。模型初始化时,我们首先初始化 [Detectron2](https://github.com/facebookresearch/detectron2/blob/main/detectron2/config/defaults.py) 的默认设置,然后配置文件中的设置将覆盖默认设置,模型将基于更新过的设置来建立。
输入数据首先转换成 Detectron2 的类型并输入进 Detectron2 的模型中。在推理阶段,Detectron2 的模型结果将会转换回 MMDetection 的类型。
## 使用 Detectron2 的预训练权重
`Detectron2Wrapper` 中的权重初始化将不使用 MMDetection 的逻辑。用户可以设置 `model.d2_detector.weights=xxx` 来加载预训练的权重。
例如,我们可以使用 `model.d2_detector.weights='detectron2://ImageNetPretrained/MSRA/R-50.pkl'` 来加载 ResNet-50 的预训练权重,或者使用
`model.d2_detector.weights='detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x/137260431/model_final_a54504.pkl'` 来加载 Detectron2 中提出的预训练的Mask R-CNN权重。
**注意:** 不能直接使用 `load_from` 来加载 Detectron2 的预训练模型,但可以通过 `tools/model_converters/detectron2_to_mmdet.py` 先对该预训练模型进行转换。
在测试时,用户应该首先使用 `tools/model_converters/detectron2_to_mmdet.py` 将 Detectron2 的预训练权重转换为 MMDetection 可读取的结构。
```shell
python tools/model_converters/detectron2_to_mmdet.py ${Detectron2 ckpt path} ${MMDetectron ckpt path}
```
基础概念
***************
.. toctree::
:maxdepth: 1
data_flow.md
structures.md
models.md
datasets.md
transforms.md
evaluation.md
engine.md
conventions.md
组件定制
************************
.. toctree::
:maxdepth: 1
customize_models.md
customize_losses.md
customize_dataset.md
customize_transforms.md
customize_runtime.md
How to
************************
.. toctree::
:maxdepth: 1
how_to.md
# 数据结构(待更新)
# 数据变换(待更新)
按照惯例,我们使用 `Dataset``DataLoader` 进行多进程的数据加载。`Dataset` 返回字典类型的数据,数据内容为模型 `forward` 方法的各个参数。由于在目标检测中,输入的图像数据具有不同的大小,我们在 `MMCV` 里引入一个新的 `DataContainer` 类去收集和分发不同大小的输入数据。更多细节请参考[这里](https://github.com/open-mmlab/mmcv/blob/master/mmcv/parallel/data_container.py)
数据的准备流程和数据集是解耦的。通常一个数据集定义了如何处理标注数据(annotations)信息,而一个数据流程定义了准备一个数据字典的所有步骤。一个流程包括一系列的操作,每个操作都把一个字典作为输入,然后再输出一个新的字典给下一个变换操作。
我们在下图展示了一个经典的数据处理流程。蓝色块是数据处理操作,随着数据流程的处理,每个操作都可以在结果字典中加入新的键(标记为绿色)或更新现有的键(标记为橙色)。
![pipeline figure](../../../resources/data_pipeline.png)
这些操作可以分为数据加载(data loading)、预处理(pre-processing)、格式变化(formatting)和测试时数据增强(test-time augmentation)。
下面的例子是 `Faster R-CNN` 的一个流程:
```python
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='Resize', img_scale=(1333, 800), keep_ratio=True),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='Normalize', **img_norm_cfg),
dict(type='Pad', size_divisor=32),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']),
]
test_pipeline = [
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(1333, 800),
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(type='Normalize', **img_norm_cfg),
dict(type='Pad', size_divisor=32),
dict(type='ImageToTensor', keys=['img']),
dict(type='Collect', keys=['img']),
])
]
```
mmdet.apis
--------------
.. automodule:: mmdet.apis
:members:
mmdet.datasets
--------------
datasets
^^^^^^^^^^
.. automodule:: mmdet.datasets
:members:
api_wrappers
^^^^^^^^^^^^^^^^^
.. automodule:: mmdet.datasets.api_wrappers
:members:
samplers
^^^^^^^^^^
.. automodule:: mmdet.datasets.samplers
:members:
transforms
^^^^^^^^^^^^
.. automodule:: mmdet.datasets.transforms
:members:
mmdet.engine
--------------
hooks
^^^^^^^^^^
.. automodule:: mmdet.engine.hooks
:members:
optimizers
^^^^^^^^^^^^^^^
.. automodule:: mmdet.engine.optimizers
:members:
runner
^^^^^^^^^^
.. automodule:: mmdet.engine.runner
:members:
schedulers
^^^^^^^^^^^^^^^^^
.. automodule:: mmdet.engine.schedulers
:members:
mmdet.evaluation
--------------------
functional
^^^^^^^^^^^^^^^^^
.. automodule:: mmdet.evaluation.functional
:members:
metrics
^^^^^^^^^^
.. automodule:: mmdet.evaluation.metrics
:members:
mmdet.models
--------------
backbones
^^^^^^^^^^^^^^^^^^
.. automodule:: mmdet.models.backbones
:members:
data_preprocessors
^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: mmdet.models.data_preprocessors
:members:
dense_heads
^^^^^^^^^^^^^^^
.. automodule:: mmdet.models.dense_heads
:members:
detectors
^^^^^^^^^^
.. automodule:: mmdet.models.detectors
:members:
layers
^^^^^^^^^^
.. automodule:: mmdet.models.layers
:members:
losses
^^^^^^^^^^
.. automodule:: mmdet.models.losses
:members:
necks
^^^^^^^^^^^^
.. automodule:: mmdet.models.necks
:members:
roi_heads
^^^^^^^^^^^^^
.. automodule:: mmdet.models.roi_heads
:members:
seg_heads
^^^^^^^^^^^^^
.. automodule:: mmdet.models.seg_heads
:members:
task_modules
^^^^^^^^^^^^^
.. automodule:: mmdet.models.task_modules
:members:
test_time_augs
^^^^^^^^^^^^^^^^^^^^
.. automodule:: mmdet.models.test_time_augs
:members:
utils
^^^^^^^^^^
.. automodule:: mmdet.models.utils
:members:
mmdet.structures
--------------------
structures
^^^^^^^^^^^^^^^^^
.. automodule:: mmdet.structures
:members:
bbox
^^^^^^^^^^
.. automodule:: mmdet.structures.bbox
:members:
mask
^^^^^^^^^^
.. automodule:: mmdet.structures.mask
:members:
mmdet.testing
----------------
.. automodule:: mmdet.testing
:members:
mmdet.visualization
--------------------
.. automodule:: mmdet.visualization
:members:
mmdet.utils
--------------
.. automodule:: mmdet.utils
:members:
## 中文解读文案汇总(待更新)
### 1 官方解读文案(v2.x)
#### 1.1 框架解读
- **[轻松掌握 MMDetection 整体构建流程(一)](https://zhuanlan.zhihu.com/p/337375549)**
- **[轻松掌握 MMDetection 整体构建流程(二)](https://zhuanlan.zhihu.com/p/341954021)**
- **[轻松掌握 MMDetection 中 Head 流程](https://zhuanlan.zhihu.com/p/343433169)**
#### 1.2 算法解读
- **[轻松掌握 MMDetection 中常用算法(一):RetinaNet 及配置详解](https://zhuanlan.zhihu.com/p/346198300)**
- **[轻松掌握 MMDetection 中常用算法(二):Faster R-CNN|Mask R-CNN](https://zhuanlan.zhihu.com/p/349807581)**
- [轻松掌握 MMDetection 中常用算法(三):FCOS](https://zhuanlan.zhihu.com/p/358056615)
- [轻松掌握 MMDetection 中常用算法(四):ATSS](https://zhuanlan.zhihu.com/p/358125611)
- [轻松掌握 MMDetection 中常用算法(五):Cascade R-CNN](https://zhuanlan.zhihu.com/p/360952172)
- [轻松掌握 MMDetection 中常用算法(六):YOLOF](https://zhuanlan.zhihu.com/p/370758213)
- [轻松掌握 MMDetection 中常用算法(七):CenterNet](https://zhuanlan.zhihu.com/p/374891478)
- [轻松掌握 MMDetection 中常用算法(八):YOLACT](https://zhuanlan.zhihu.com/p/376347955)
- [轻松掌握 MMDetection 中常用算法(九):AutoAssign](https://zhuanlan.zhihu.com/p/378581552)
- [YOLOX 在 MMDetection 中复现全流程解析](https://zhuanlan.zhihu.com/p/398545304)
- [喂喂喂!你可以减重了!小模型 - MMDetection 新增SSDLite 、 MobileNetV2YOLOV3 两大经典算法](https://zhuanlan.zhihu.com/p/402781143)
#### 1.3 工具解读
- [OpenMMLab 中混合精度训练 AMP 的正确打开方式](https://zhuanlan.zhihu.com/p/375224982)
- [小白都能看懂!手把手教你使用混淆矩阵分析目标检测](https://zhuanlan.zhihu.com/p/443499860)
- [MMDetection 图像缩放 Resize 详细说明 OpenMMLab](https://zhuanlan.zhihu.com/p/381117525)
- [拿什么拯救我的 4G 显卡](https://zhuanlan.zhihu.com/p/430123077)
- [MMDet居然能用MMCls的Backbone?论配置文件的打开方式](https://zhuanlan.zhihu.com/p/436865195)
#### 1.4 知乎问答
- [COCO数据集上1x模式下为什么不采用多尺度训练?](https://www.zhihu.com/question/462170786/answer/1915119662)
- [MMDetection中SOTA论文源码中将训练过程中BN层的eval打开?](https://www.zhihu.com/question/471189603/answer/2195540892)
- [基于PyTorch的MMDetection中训练的随机性来自何处?](https://www.zhihu.com/question/453511684/answer/1839683634)
- [单阶段、双阶段、anchor-based、anchor-free 这四者之间有什么联系吗?](https://www.zhihu.com/question/428972054/answer/1619925296)
- [目标检测的深度学习方法,有推荐的书籍或资料吗?](https://www.zhihu.com/question/391577080/answer/1612593817)
- [大佬们,刚入学研究生,想入门目标检测,有什么学习路线可以入门的?](https://www.zhihu.com/question/343768934/answer/1612580715)
- [目标检测领域还有什么可以做的?](https://www.zhihu.com/question/280703314/answer/1627885518)
- [如何看待Transformer在CV上的应用前景,未来有可能替代CNN吗?](https://www.zhihu.com/question/437495132/answer/1686380553)
- [MMDetection如何学习源码?](https://www.zhihu.com/question/451585041/answer/1832498963)
- [如何具体上手实现目标检测呢?](https://www.zhihu.com/question/341401981/answer/1848561187)
#### 1.5 其他
- **[不得不知的 MMDetection 学习路线(个人经验版)](https://zhuanlan.zhihu.com/p/369826931)**
- [OpenMMLab 社区专访之 YOLOX 复现篇](https://zhuanlan.zhihu.com/p/405913343)
### 2 社区解读文案(v2.x)
- [手把手带你实现经典检测网络 Mask R-CNN 的推理](https://zhuanlan.zhihu.com/p/414082071)
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import subprocess
import sys
import pytorch_sphinx_theme
sys.path.insert(0, os.path.abspath('../../'))
# -- Project information -----------------------------------------------------
project = 'MMDetection'
copyright = '2018-2021, OpenMMLab'
author = 'MMDetection Authors'
version_file = '../../mmdet/version.py'
def get_version():
with open(version_file, 'r') as f:
exec(compile(f.read(), version_file, 'exec'))
return locals()['__version__']
# The full version, including alpha/beta/rc tags
release = get_version()
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.viewcode',
'myst_parser',
'sphinx_markdown_tables',
'sphinx_copybutton',
]
myst_enable_extensions = ['colon_fence']
myst_heading_anchors = 3
autodoc_mock_imports = [
'matplotlib', 'pycocotools', 'terminaltables', 'mmdet.version', 'mmcv.ops'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
source_suffix = {
'.rst': 'restructuredtext',
'.md': 'markdown',
}
# The main toctree document.
master_doc = 'index'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# html_theme = 'sphinx_rtd_theme'
html_theme = 'pytorch_sphinx_theme'
html_theme_path = [pytorch_sphinx_theme.get_html_theme_path()]
html_theme_options = {
'menu': [
{
'name': 'GitHub',
'url': 'https://github.com/open-mmlab/mmdetection'
},
],
# Specify the language of shared menu
'menu_lang':
'cn',
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_css_files = ['css/readthedocs.css']
language = 'zh_CN'
# -- Extension configuration -------------------------------------------------
# Ignore >>> when copying code
copybutton_prompt_text = r'>>> |\.\.\. '
copybutton_prompt_is_regexp = True
def builder_inited_handler(app):
subprocess.run(['./stat.py'])
def setup(app):
app.connect('builder-inited', builder_inited_handler)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment