MMDetection 开发规范详解:图像尺寸约定、Loss 返回结构、空 Proposal 处理与 COCO Panoptic 约定
2026/9/20 11:54:31 网站建设 项目流程
  • 人工智能
  • 计算机视觉
  • 深度学习
  • 模型评测

【免费下载链接】mmdetection

OpenMMLab Detection Toolbox and Benchmark

项目地址:https://gitcode.com/gh_mirrors/mm/mmdetection
点击查看免费下载

导读

本文以 MMDetection 官方文档 docs/en/advanced_guides/conventions.md 为主线,系统梳理在 OpenMMLab 2.0 架构下自定义检测器时必知的四类约定:数据变换管线中的图像尺寸顺序、model(**data)返回的 loss 字典结构、两阶段检测器对空 Proposal 的专项处理、以及 COCO Panoptic 数据集的标签与结果编码约定。读完本文,你将掌握如何正确编写自定义 Transform 与 RoIHead、如何理解并扩展 loss 回传机制、如何安全处理空 batch,以及 Panoptic 任务中标签语义与结果解码的正确姿势,从而避免在二次开发中最常见的一类隐性 bug。

一、图像尺寸顺序约定:(width, height)(height, width)的分界线

1.1 为什么会有两套顺序

在 OpenMMLab 2.0(即 MMDetection 3.x 系列)中,图像形状参数的顺序存在刻意区分的两套约定:

  • 数据变换管线的构造参数:为了与 OpenCV 的输入参数习惯保持一致,所有关于图像形状的初始化参数一律使用(width, height)顺序。例如Resize(scale=(1333, 800))Mosaic(img_scale=(640, 640))中的宽在前、高在后;
  • 数据管线中流转的字段与模型内部:为了计算方便,经过数据管线与模型的所有形状字段一律使用(height, width)顺序。

这样设计的原因很实际:OpenCV 的resizeimresize等接口的第一个参数是(width, height),直接透传能减少转换出错;而 NumPy/Tensor 的 shape 天然是(H, W, C),用(height, width)可以直接与张量形状对齐。

1.2 管线中各形状字段的含义

在数据变换管线处理后的结果 dict 中,与形状相关的字段及其取值含义如下(均为(height, width)):

字段含义顺序
img_shape变换后(如 Resize、Mosaic 后)图像的高宽(height, width)
ori_shape原始图像的高宽(height, width)
pad_shapePadding 之后图像的高宽(height, width)
batch_input_shapebatch 内统一 padding 后的高宽(height, width)

其中batch_input_shapepad_shape并不是在数据变换管线里产生的,而是由数据预处理器在模型前向时写入的。在 mmdet/models/data_preprocessors/data_preprocessor.py 中,DetDataPreprocessor会基于inputs[0].size()[-2:]计算batch_input_shape,并将 batch 内每张图实际 pad 后的形状记为pad_shape,写入每个data_sample的 meta 信息:

batch_input_shape = tuple(inputs[0].size()[-2:]) for data_sample, pad_shape in zip(data_samples, batch_pad_shape): data_sample.set_metainfo({ 'batch_input_shape': batch_input_shape, 'pad_shape': pad_shape })

值得注意的是,pad_shape逐图的(因为AspectRatioBatchSampler允许同一 batch 内不同图按各自长宽比 pad),而batch_input_shape整个 batch 统一的。这两者都是 (height, width) 顺序。

1.3 以 Mosaic 为例:参数与结果的顺序对照

文档以Mosaic变换为典型示例。其构造参数img_scale(width, height)顺序,而写入结果 dict 的img_shape(height, width)顺序:

@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 order should be (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]

对照 mmdet/datasets/transforms/transforms.py 中的真实实现,可以看得更清楚:

  • img_scale声明为(width, height),因此创建 mosaic 画布时使用np.full((int(self.img_scale[1] * 2), int(self.img_scale[0] * 2), 3), ...),即先取img_scale[1](高)再取img_scale[0](宽);
  • 中心点采样center_x = random.uniform(*self.center_ratio_range) * self.img_scale[0]center_y = ... * self.img_scale[1],同样遵循 x 对应宽、y 对应高;
  • 最终results['img_shape'] = mosaic_img.shape[:2]直接取 NumPy shape,天然是(height, width)
  • 子图 keep-ratio resize 时scale_ratio_i = min(self.img_scale[1] / h_i, self.img_scale[0] / w_i),也是用img_scale[1]与高度比、img_scale[0]与宽度比;
  • 边界裁剪mosaic_bboxes.clip_([2 * self.img_scale[1], 2 * self.img_scale[0]])同样是 (h, w) 顺序传入。

这类约定在仓库中还有一套辅助校验:mmdet/utils提供的log_img_scale工具(Mosaic构造时以shape_order='wh'调用),会在配置了非方形img_scale时打印提示日志,帮助开发者第一时间发现顺序混淆。

在 tests/test_datasets/test_transforms/test_transforms.py 中,TestMosaic覆盖了多种校验:Mosaic(img_scale=640)(非 tuple)会触发AssertionErrorMosaic(prob=1.5)超出[0, 1]范围会触发AssertionError,且各测试均断言results['img_shape'] == results['img'].shape[:2],把“结果字段必须是 (H, W)”固化成了回归测试。

1.4 给自定义 Transform 作者的检查清单

当你编写自定义数据变换时,请按以下清单自检:

  1. 构造参数中表示图像尺寸的元组,一律写成(width, height),并在 docstring 中注明 "The shape order should be (width, height)";
  2. 输出到结果 dict 的img_shapeori_shapepad_shape等字段,一律取(height, width)
  3. 使用 OpenCV / mmcv 的 resize 类接口时,目标尺寸参数传(width, height);读写 NumPy 数组时用shape[:2]得到(height, width),不要混用。

二、Loss 约定:以 dict 返回、按 key 回传

2.1model(**data)返回 loss dict

在 MMDetection 中,训练时model(**data)会返回一个包含 loss 与指标(metric)的 dict。该行为由 mmdet/models/detectors/base.py 中BaseDetector.forwardmode='loss'分支触发:return self.loss(inputs, data_samples)。也就是说,loss()抽象方法的返回类型是Union[dict, tuple],而各检测器的loss()内部会把各个 head 返回的 loss 汇总成一个 dict。

以 bbox head 为例,其loss()方法(mmdet/models/roi_heads/bbox_heads/bbox_head.py)的返回结构如下:

class BBoxHead(nn.Module): ... def loss(self, ...): losses = dict() # classification loss losses['loss_cls'] = self.loss_cls(...) # classification accuracy losses['acc'] = accuracy(cls_score, labels) # bbox regression loss losses['loss_bbox'] = self.loss_bbox(...) return losses

bbox_head.loss()会在模型前向(loss()方法)过程中被调用。返回的 dict 包含三个 key:'loss_bbox''loss_cls''acc'

2.2 只有 key 含loss的项参与反传

这是本小节最核心的一条规则:

  • 'loss_bbox''loss_cls'是真正的损失项,会参与反向传播
  • 'acc'只是分类精度指标,仅用于监控训练过程,不参与反向传播。

默认情况下,框架只对 key 中包含'loss'的项做反向传播。这一行为由BaseDetector.train_step()控制——即基类中负责梯度回传与参数更新的训练入口(forward本身只负责计算,不做反传与参数更新,这一点在 base.py 的 docstring 中有明确说明)。

如果你想改变“只有含loss的 key 才回传”的默认行为,文档明确指出:修改BaseDetector.train_step()即可。例如自定义一个带额外约束项(如中间层特征正则、辅助自监督任务)的检测器时,可以覆写train_step(),将额外的张量项并入回传列表,或调整 loss 加权逻辑。

补充一个工程细节:mmdet/engine/hooks/checkloss_hook.py中的CheckInvalidLossHook会每隔interval次迭代检查outputs['loss']是否有限值,若出现 NaN/Inf 会立即断言失败并输出日志。这说明框架默认约定train_step汇总出的总 loss 挂在'loss'这个 key 下,自定义train_step时也应遵循该命名,以便训练监控钩子正常工作。

2.3 给自定义 Head 作者的约定

  • head 的loss()方法必须返回dict,key 命名遵循loss_xxx模式(如loss_clsloss_bboxloss_maskloss_centerness);
  • 非 loss 的监控指标(如acciou)可以放在同一 dict 中,但不要命名为loss_前缀,否则会被误当作损失参与反传;
  • 若涉及对 loss 的加权,在 head 内部用weight参数或loss_xxx.weight配置项处理,保证最终 dict 中各项仍是可直接 sum 的张量。

三、空 Proposal 约定:两阶段模型必须同时处理整 batch 空与单图空

3.1 为什么需要专门处理

两阶段检测器的 RoIHead 依赖 RPN 输出的 proposals 作为输入。在实际推理中,会频繁遇到两种情况:

  1. 整个 batch 没有任何 proposal(例如输入全是背景图);
  2. batch 中某一张图没有 proposal,但其他图有。

如果不做特殊处理,空张量参与后续的bbox2roipredict_by_feat、级联 refine 等流程会引发形状不匹配的报错。因此 MMDetection 对两阶段模型的空 proposals 做了专门处理,并提供单元测试覆盖。

3.2 CascadeRoIHead 中的处理范式

文档以 mmdet/models/roi_heads/cascade_roi_head.py 的simple_test为例,给出两段处理逻辑。

第一段:处理整 batch 无 proposal 的情况

# simple_test method ... # There is no proposal in the whole batch 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

rois.shape[0] == 0roisbbox2roi拼接后的结果,行数为 0 即整个 batch 无任何 proposal)时,直接按类别数量构造空结果:每个类别对应一个(0, 5)的空数组(5 列对应[x1, y1, x2, y2, score]),mask 分支则用空列表占位,然后提前返回,不再进入后续的 refine 与预测流程。

第二段:处理单张图无 proposal 的情况(级联 refine 阶段)

# There is no proposal in the single image for i in range(self.num_stages): ... if i < self.num_stages - 1: for j in range(num_imgs): # Handle empty proposal 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, bbox_pred[j], img_metas[j]) refine_roi_list.append(refine_roi)

在级联阶段之间,对每张图单独判断rois[j].shape[0] > 0:只有非空图才执行regress_by_class精修 proposal;空图直接跳过,从而避免对空张量做按类回归。

3.3 当前仓库实现中的演进与验证

需要说明的是,在本文所基于的 MMDetection 3.x 源码中,上述范式进一步演进为更统一的empty_instances()工具。在 cascade_roi_head.py 的predict_bbox中:

num_proposals_per_img = tuple(len(p) for p in proposals) rois = bbox2roi(proposals) if rois.shape[0] == 0: return empty_instances( batch_img_metas, rois.device, task_type='bbox', box_type=self.bbox_head[-1].predict_box_type, num_classes=self.bbox_head[-1].num_classes, score_per_cls=rcnn_test_cfg is None)

empty_instances会按num_classes生成空预测结果(bboxes/labels/scores/masks 均为空),其输出可直接被DetDataSample消费;同理predict_mask中也有if mask_rois.shape[0] == 0的空处理分支(cascade_roi_head.py)。

给自定义 RoIHead 作者的建议(文档明确给出的指引):如果你实现了自定义RoIHead,请参照上述方式处理空 proposals:

  1. 在拼接rois之后,先判断rois.shape[0] == 0,用empty_instances或手工构造空结果提前返回;
  2. 在逐图循环、逐 stage 循环中,对每张图的rois[j].shape[0] > 0做判空,保证空图不进入 refine/regress_by_class 等依赖非空张量的算子;
  3. 同时覆盖“整 batch 空”与“单图空”两个层次,缺一不可。

四、COCO Panoptic 数据集约定:标签语义与结果编码

4.1 语义分割标签的 VOID 约定变迁

MMDetection 对CocoPanopticDataset的实现约定如下(这是与普通检测标签最不同、也最容易被忽略的一点):

  1. mmdet ≤ 2.16.0:语义分割中的前景/背景标签范围与 MMDetection 默认设置不同——标签0表示VOID(空洞/忽略)标签,类别标签从1开始;
  2. 自 mmdet = 2.17.0 起:为了与边界框标签保持一致,语义分割的类别标签改为从0开始,标签255表示VOID

也就是说,在 3.x 系列中,语义分割的类别编号与 bbox 的类别编号统一:0 ~ num_classes-1是有效类别,255是忽略区域。如果从旧版本(2.16.0 及以前)迁移自定义数据集或训练脚本,务必核对标注文件中的标签语义,否则会出现“背景变类别、VOID 被当成真值”的严重错误。

4.2 Pad 管线对 seg 填充值的支持

为了支持上述255作为 VOID 的约定,Pad变换专门提供了对seg(语义分割图)设置填充值的能力。在 mmdet/datasets/transforms/transforms.py 中,Padpad_val参数支持两种形式:

  • 单个数值:用于 pad 图像,同时语义分割图固定用255填充(这正是为了保持 VOID 语义,避免 pad 区域被当作有效类别参与 loss);
  • dict 形式:可分别为不同字段指定填充值,其中seg字段通常应配置为255

在 panoptic 训练中,Pad之后由数据预处理器进一步执行pad_gt_sem_seg(data_preprocessor.py),同样按batch_input_shape以 VOID 值填充语义分割真值,保证 loss 计算时忽略 pad 区域。

4.3 结果图的编码公式

  1. 评估阶段:panoptic 结果是与原始图像同尺寸的一张图(map)。结果图中每个像素值的编码格式为:
panoptic_value = instance_id * INSTANCE_OFFSET + category_id

instance_id * INSTANCE_OFFSET + category_id。在 mmdet/evaluation/functional/panoptic_utils.py 中:

# pan_id = ins_id * INSTANCE_OFFSET + cat_id INSTANCE_OFFSET = 1000

INSTANCE_OFFSET取值为 1000。解码时:对结果图的像素值整除 1000 得到实例 id(ins_id),取模 1000 得到类别 id(cat_id)。这样单个像素同时编码了“属于哪个实例”和“属于哪个语义类别”,实例分割与语义分割共用一张图。

4.4 配套实现与配置文件

CocoPanopticDataset的实现位于 mmdet/datasets/coco_panoptic.py,继承自CocoDataset,其data_prefixseg前缀指向 panoptic 分割图目录,METAINFO同时定义了classes(含 thing 与 stuff)与thing_classes。完整的训练/验证配置可参考 configs/base/datasets/coco_panoptic.py:

  • 训练管线使用LoadImageFromFile+LoadPanopticAnnotations+Resize(scale=(1333, 800), keep_ratio=True)+RandomFlip+PackDetInputs
  • 数据路径中ann_file='annotations/panoptic_train2017.json'data_prefix=dict(img='train2017/', seg='annotations/panoptic_train2017/')
  • 评估器使用CocoPanopticMetric,传入ann_fileseg_prefix

单测方面,tests/test_datasets/test_coco_panoptic.py 对CocoPanopticDataset的加载、panoptic json 中的重复 id 处理、以及filter_cfg=dict(filter_empty_gt=True, min_size=32)下的行为均有覆盖;结合 configs 与 panoptic_utils.py,可以完整还原从数据加载到 PQ 指标计算的整条链路。

五、总结:二次开发前必读的四条约定

约定核心要点主要出处
图像尺寸顺序构造参数(width, height);管线字段与模型内部(height, width)img_shape/ori_shape/pad_shape/batch_input_shape均为 (H, W)mmdet/datasets/transforms/transforms.py、data_preprocessor.py
Loss 返回结构model(**data)返回 dict;仅 key 含loss的项参与反传;acc等仅为监控指标;修改train_step()可改变回传行为base.py、bbox_head.py
空 Proposal同时处理整 batch 空(rois.shape[0] == 0)与单图空(rois[j].shape[0] > 0cascade_roi_head.py
COCO Panoptic3.x 起类别标签从 0 开始、255 为 VOID;结果像素编码ins_id * 1000 + cat_idcoco_panoptic.py、panoptic_utils.py

这些约定不是“风格建议”,而是框架硬性契约:自定义 Transform、Head、RoIHead 或迁移老版本数据时,违反其中任何一条都会导致难以排查的维度错误、梯度异常或指标失真。建议在动手改代码前,先对照本文四条约定逐项自检;如果只是基于现有算法做配置层面的二次开发,则重点关注第一节与第四节的内容即可。

  • 人工智能
  • 计算机视觉
  • 深度学习
  • 模型评测

【免费下载链接】mmdetection

OpenMMLab Detection Toolbox and Benchmark

项目地址:https://gitcode.com/gh_mirrors/mm/mmdetection
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询