- 人工智能
- 计算机视觉
- 深度学习
- 模型评测
【免费下载链接】mmdetection
OpenMMLab Detection Toolbox and Benchmark
导读
本文以 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 的resize、imresize等接口的第一个参数是(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_shape | Padding 之后图像的高宽 | (height, width) |
batch_input_shape | batch 内统一 padding 后的高宽 | (height, width) |
其中batch_input_shape与pad_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)会触发AssertionError、Mosaic(prob=1.5)超出[0, 1]范围会触发AssertionError,且各测试均断言results['img_shape'] == results['img'].shape[:2],把“结果字段必须是 (H, W)”固化成了回归测试。
1.4 给自定义 Transform 作者的检查清单
当你编写自定义数据变换时,请按以下清单自检:
- 构造参数中表示图像尺寸的元组,一律写成
(width, height),并在 docstring 中注明 "The shape order should be (width, height)"; - 输出到结果 dict 的
img_shape、ori_shape、pad_shape等字段,一律取(height, width); - 使用 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.forward的mode='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 lossesbbox_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_cls、loss_bbox、loss_mask、loss_centerness); - 非 loss 的监控指标(如
acc、iou)可以放在同一 dict 中,但不要命名为loss_前缀,否则会被误当作损失参与反传; - 若涉及对 loss 的加权,在 head 内部用
weight参数或loss_xxx.weight配置项处理,保证最终 dict 中各项仍是可直接 sum 的张量。
三、空 Proposal 约定:两阶段模型必须同时处理整 batch 空与单图空
3.1 为什么需要专门处理
两阶段检测器的 RoIHead 依赖 RPN 输出的 proposals 作为输入。在实际推理中,会频繁遇到两种情况:
- 整个 batch 没有任何 proposal(例如输入全是背景图);
- batch 中某一张图没有 proposal,但其他图有。
如果不做特殊处理,空张量参与后续的bbox2roi、predict_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] == 0(rois是bbox2roi拼接后的结果,行数为 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:
- 在拼接
rois之后,先判断rois.shape[0] == 0,用empty_instances或手工构造空结果提前返回; - 在逐图循环、逐 stage 循环中,对每张图的
rois[j].shape[0] > 0做判空,保证空图不进入 refine/regress_by_class 等依赖非空张量的算子; - 同时覆盖“整 batch 空”与“单图空”两个层次,缺一不可。
四、COCO Panoptic 数据集约定:标签语义与结果编码
4.1 语义分割标签的 VOID 约定变迁
MMDetection 对CocoPanopticDataset的实现约定如下(这是与普通检测标签最不同、也最容易被忽略的一点):
- mmdet ≤ 2.16.0:语义分割中的前景/背景标签范围与 MMDetection 默认设置不同——标签
0表示VOID(空洞/忽略)标签,类别标签从1开始; - 自 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 中,Pad的pad_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 结果图的编码公式
- 评估阶段: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 = 1000INSTANCE_OFFSET取值为 1000。解码时:对结果图的像素值整除 1000 得到实例 id(ins_id),取模 1000 得到类别 id(cat_id)。这样单个像素同时编码了“属于哪个实例”和“属于哪个语义类别”,实例分割与语义分割共用一张图。
4.4 配套实现与配置文件
CocoPanopticDataset的实现位于 mmdet/datasets/coco_panoptic.py,继承自CocoDataset,其data_prefix中seg前缀指向 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_file与seg_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] > 0) | cascade_roi_head.py |
| COCO Panoptic | 3.x 起类别标签从 0 开始、255 为 VOID;结果像素编码ins_id * 1000 + cat_id | coco_panoptic.py、panoptic_utils.py |
这些约定不是“风格建议”,而是框架硬性契约:自定义 Transform、Head、RoIHead 或迁移老版本数据时,违反其中任何一条都会导致难以排查的维度错误、梯度异常或指标失真。建议在动手改代码前,先对照本文四条约定逐项自检;如果只是基于现有算法做配置层面的二次开发,则重点关注第一节与第四节的内容即可。
- 人工智能
- 计算机视觉
- 深度学习
- 模型评测
【免费下载链接】mmdetection
OpenMMLab Detection Toolbox and Benchmark
相关推荐
XiaoMi-Pro-Hackintosh性能调优:CPU频率管理与温度控制终极指南
XiaoMi Pro Hackintosh性能调优:CPU频率管理与温度控制终极指南 XiaoMi Pro Hackintosh项目为小米笔记本Pro系列提供了
固件驱动开发Process Hacker项目开发指南:构建规范与编码约定详解
Process Hacker项目开发指南:构建规范与编码约定详解 项目概述 Process Hacker是一个功能强大的系统监控工具,它提供了对进程、线程、服务
桌面应用调试器应用安全驱动开发实验记录完全指南:ma-gym Monitor 包装器的统计、视频与多智能体日志用法
实验记录完全指南:ma gym Monitor 包装器的统计、视频与多智能体日志用法 📊 本文带你完整掌握 ma gym Monitor 包装器 的用法:它为
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考