☰
MMDetection数据输入全解析:从配置到前向的每一步
2026/10/16 2:42:44 网站建设 项目流程

很多人第一次跑 MMDetection 的 demo,训练脚本一敲,日志刷得飞快,验证集精度居然还挺正常。但你要是追问他:一张图片从硬盘读进内存,再到模型前向,这中间到底走了几步?多半就答不上来了。这不怪你,因为整个数据输入链路确实藏得很深,config 里面一个train_pipeline就把所有细节都盖住了。这篇笔记专门把 MMDetection 的数据输入从头到尾拆一遍,重点讲数据类、pipeline 流水线、训练与测试的差异、以及新数据集接入时最少改哪几处。适合刚入门目标检测、打算自己改数据流程或者接新数据集的同学参考。

1. 数据模块整体架构:先别急着跑 demo,把链路捋清楚

很多人习惯先把环境配好、demo 跑通再说,这没问题。但到了改数据集、调增强、查KeyError的时候,如果脑子里没有一张数据流图,就会非常痛苦。我建议你花十分钟把这条链路记在心里。

1.1 Config 到 Dataset:注册器是怎么把配置变成对象的

MMDetection 整个框架建立在注册器机制上。所谓注册器,你可以把它理解成一本点餐单:每个类写好之后,往单子上登记一个名字,之后配置里写这个名字,框架就会自动找到对应的类并实例化。

在数据这一侧,最常遇到的名字就是CocoDataset、CustomDataset、mmdet内置的各种 pipeline 类。config 文件里写dataset_type = 'CocoDataset',build_dataset就会去注册表里查CocoDataset这个类,然后把用户写的参数传进去。这个过程说起来抽象,实际上就是查字典 + 反射调用,关键好处是:你不改代码也能通过配置文件换数据集、换增强策略。

来看一个典型训练配置里的 dataloader 片段:

train_dataloader = dict( batch_size=8, num_workers=4, persistent_workers=True, sampler=dict(type='DefaultSampler', shuffle=True), dataset=dict( type='CocoDataset', data_root='data/coco/', ann_file='annotations/instances_train2017.json', data_prefix=dict(img='train2017/'), pipeline= [...] # 训练流水线 ) )

这里dataset会被构建成CocoDataset实例,pipeline会被单独构建成一个Compose对象,由多个 transform 类实例串成一条链。之后 DataLoader 每次取数据,调用的就是dataset[idx],也就是这条链上的各个 transform 依次对样本做处理。

1.2 一条数据从文件到模型,总共要过哪几道门

完整路径可以概括成六步:

  1. Config 构建 Dataset 和 Pipeline。
  2. DataLoader 按 batch_size 向 Dataset 要样本。
  3. Dataset 的__getitem__拿到一个data_info(原始样本信息字典)。
  4. Pipeline 对data_info依次做读取、增强、格式打包。
  5. DataLoader 内置的 collate 函数把多个样本堆叠成 batch。
  6. batch 送入模型,模型拿到的已经是标准化、固定尺寸的张量。

其中第 3、4 步是整条数据链的核心,也最容易出问题。很多人以为 Dataset 就是把图片读进来做个ToTensor,实际上数据增强的部分全在 pipeline 里,Dataset 本身只负责“提供原始素材”。这也是 MMDetection 和很多教科书代码不一样的地方,它的数据读取和数据处理是分离的。

2. 数据类内部到底做了什么

MMDetection 的数据类历史上改过好几版,但核心逻辑一直很稳定。搞懂这个类的结构,你后面看任何数据集适配代码都会轻松很多。

2.1 Dataset 基类里最重要的三个东西

一个 Dataset 类,翻来覆去就三件事:初始化、算长度、取样本。

__init__负责加载标注文件、建立索引、过滤坏样本。__len__返回样本数量。__getitem__是整个数据链的发动机,每次调用都会经历:

  1. 从已加载的data_list里按下标取一个data_info。
  2. 判断是训练还是测试,走prepare_train_img或prepare_test_img。
  3. 在 prepare 函数里先执行self.pipeline(data_info),把原始信息过一遍所有 transform。
  4. 返回一个经过打包的、模型可以直接消费的 dict。

简化后的骨架类似这样:

class BaseDetDataset(BaseDataset): def __getitem__(self, idx): # 先取原始数据信息 data_info = self.get_data_info(idx) # 再走 pipeline return self.pipeline(data_info) def get_data_info(self, idx): data_info = self.data_list[idx].copy() # 根据 data_prefix 拼接出绝对路径 data_info['img_path'] = osp.join(self.data_prefix.get('img', ''), data_info['img_path']) return data_info

实际代码没这么简单,但思想就是这样。注意这里 pipeline 的输入是一个 dict,输出也是一个 dict,中间所有 transform 都只对这个 dict 做修改,不产生额外对象。这样设计的好处是:你随时可以插入一个新的 transform,只要保证 key 对齐就行。

2.2 data_info 里都有哪些关键字段

拿 COCO 格式举例子。一条样本的 data_info 大概是这么个结构:

{ 'img_id': 42, 'img_path': 'train2017/000000000042.jpg', 'height': 480, 'width': 640, 'ann': { 'bboxes': array([[100, 100, 200, 300]], dtype=float32), 'labels': array([1], dtype=int64), 'segmentation': [...], 'keypoints': None, 'areas': array([...]), 'iscrowd': array([0]) } }

bboxes一定是 xyxy 格式,也就是[x1, y1, x2, y2],左上角和右下角坐标。这是 MMDetection 内部统一格式,所有 pipeline 都基于这个约定工作。如果你从别的框架迁移数据,第一件事就是把 bbox 转成 xyxy。

labels是类别编号,从 1 开始。0 通常留给背景,这在目标检测里是个不成文的约定。如果你看到某个数据集标注文件里类别从 0 开始,接入的时候一定要统一加 1,否则模型前向和 loss 计算全都会错位。

2.3 with_bbox、with_mask 这些开关是干什么的

LoadAnnotations 里有一堆with_xxx参数,很多人不重视,直接全开。我建议养成一个习惯:用什么开什么。

dict(type='LoadAnnotations', with_bbox=True, with_mask=False)

如果你只做检测,with_mask=False能省去解析分割多边形的开销。COCO 的分割标注是 RLE 或多边形,解析起来很费 CPU。有时候数据集明明只用了 bbox,却因为忘了关with_mask,训练速度慢得离谱。

还有filter_empty_gt参数,这个也很关键。它会在加载阶段直接过滤掉没有任何目标的图片。如果你的数据集里存在空标注图片,不打开这个开关,训练时模型拿到一个全是背景的样本,loss 会异常,甚至直接崩掉。

3. pipeline 流水线:数据增强的顺序就是一切

讲完 Dataset,重点来了:pipeline。这是 MMDetection 数据输入最精华的部分,也是最容易被忽略的部分。它的本质就是一系列 transform 的“流水线”,每张图从文件到模型的每一步都由它控制。

3.1 三步走:Load、Transform、Format

不管训练还是测试,pipeline 都可以分成三大段。

Load 段:LoadImageFromFile负责读图,LoadAnnotations负责读标注。这里有个容易踩的坑:LoadImageFromFile默认用 cv2 读图,读进来是 BGR 顺序。很多人之后做可视化、或者接入 RGB 预训练模型时,发现颜色不对,就是这一步埋下的隐患。

Transform 段:RandomFlip、Resize、RandomCrop、PhotoMetricDistortion、RandomBrightnessContrast等等,所有数据增强都在这里。训练策略的好坏,一半看模型结构,另一半看这段配置。

Format 段:PackDetInputs是最后一站,它会把前面处理好的结果打包成DetDataSample格式,同时把标注信息转成torch.Tensor。打包完成后,数据就被模型“接盘”了。

顺序为什么这么重要?很简单:先翻转再裁剪,和先裁剪再翻转,结果的分布完全不一样。而且标注坐标必须跟着图像同步变换。假如你先 Resize 了图片,再对 bbox 做变换,那你必须保证 Resize 的缩放系数和 bbox 的变换系数是一致的,否则位置全错。

3.2 Resize 和 RandomFlip 的坐标变换逻辑

以最常用的RandomFlip为例。水平翻转时,图片的 x 轴反向,那么 bbox 的坐标也要镜像:

x1_new = width - x1 x2_new = width - x2

注意很多教程写成x1_new = width - x2,其实两种写法结果相同,但逻辑上应该是“每个坐标点都镜像”,不是“交换左右端点”。如果你在调试的时候手推了半天的公式对不上,大概率是这里的坐标顺序搞混了。

Resize的默认策略是 keep_ratio,也就是等比缩放,短边对齐scale的短边,长边不能超过scale的长边。bbox 的变换就是一个纯比例缩放,没有旋转、没有裁剪,所以只需要把x、y、w、h分别乘上缩放系数就行。这里有个隐藏参数bbox_clip_border,默认 True,会把跑到图片边界外的 bbox 裁剪回界内。如果关了,负坐标的 bbox 就会往下游传,后面算 IoU 时全是负数,loss 直接 nan,新手经常在这里翻车。

dict( type='RandomFlip', prob=0.5, direction='horizontal')

如果数据集里存在竖长条目标,可以考虑direction='horizontal'和'vertical'都开。但从经验看,水平翻转已经带来足够的多样性,垂直翻转不是必须的,而且有些任务(比如车牌检测)翻转后文字倒置,反而不合理。

3.3 建议你手动抓一张图跑一遍 pipeline

我每次接手新项目,第一件事不是看模型结构,而是手动抓一张图,单独跑一遍 pipeline,看看中间结果长什么样。这个习惯让我避开了一大半数据相关的坑。

可以用一个很短的程序做到:

from mmengine.config import Config from mmdet.datasets import build_dataset cfg = Config.fromfile('configs/your_config.py') dataset = build_dataset(cfg.train_dataloader.dataset) data = dataset[0] # 第一张训练图 print(data.keys()) print(data['data_samples'].gt_instances.bboxes[:5])

这一步能看到数据增强后的 bbox 长什么样、shape 对不对、有没有越界。如果输出全是nan或者空 tensor,那就说明数据链哪一环断了,赶紧回头查。

4. 训练与测试的数据差异,以及批量采样内部机制

训练和测试的 pipeline 长得不一样,这不是偷懒,是设计使然。理解这种差异,你对“数据增强的度”会有更深的认识。

4.1 为什么 test_pipeline 里没有那些增强

看一个典型的测试 pipeline:

test_pipeline = [ dict(type='LoadImageFromFile'), dict(type='Resize', scale=(1333, 800), keep_ratio=True), dict(type='LoadAnnotations', with_bbox=True), dict(type='PackDetInputs', meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape', 'scale_factor')) ]

训练里常见的翻转、裁剪、颜色抖动,这里一个都没有。原因有三点:

第一是复现性。测试结果必须可复现,不能今天跑和明天跑差几个点。第二是公平性。排行榜上的分数应该反映模型能力,而不是数据增强的运气。第三是稳定性。固定 scale 能确保所有测试图经过统一的尺寸调整,输出的预测框尺度一致,后处理 NMS 的阈值设置才可能有意义。

另外注意,测试阶段依然有LoadAnnotations。很多人问测试不是不需要标注吗?确实不需要参与前向,但评测时需要计算 mAP,所以标注还是要加载进来,只是不会传给模型,而是在PackDetInputs里单独存一份。

4.2 尺度控制与多尺度训练:尺寸随机性反而是你的朋友

目标检测任务的输入尺寸不像分类任务那么死板。Resize的scale=(1333, 800)意思是:短边不超过 800,长边不超过 1333,同时保持宽高比。训练的时候,如果固定这个值,每张图都会被缩放到相同的“尺度约束”下,但这并不意味着每张图 final 尺寸一致,因为不同图的宽高比不同,resize 之后实际像素尺寸可能差不少。

多尺度训练的配置更直接把随机性写进去:

dict( type='RandomResize', scale=(1333, 640), ratio_range=None, keep_ratio=True, )

这里scale=(1333, 640)表示短边从 640 到 1333 之间随机取,长边按比例适配。实际效果是模型在一个 batch 里看到不同尺度的目标,对小目标、大目标的鲁棒性都会变好。代价也很直接:训练时间变长、显存波动、batch 内样本尺寸不一致导致 collate 复杂度上升。

我试过很多项目,多尺度训练带来的精度提升通常有 1 到 2 个点,但它对 batch 内 padding 策略要求很高。好在 MMDetection 内部已经处理了按尺寸 batch 分组的问题,你只要保证显存够就行。

4.3 DefaultSampler 与 collate:批量喂给模型前的最后一步

Sampler 负责决定每个 epoch 里样本的访问顺序。配置里最常见的DefaultSampler会在构造时按seed生成随机序列,保证训练可复现。shuffle=True是训练标配,测试时一般设 False。

真正的“无声英雄”是 collate。DataLoader 取出 batch_size 个样本后,collate 函数负责把它们拼成一个 batch。MMDetection 的 collate 会按字段做处理:图像堆叠成四维张量(B, C, H, W),bbox 按列表长度补齐,元信息保持原样。这里有个细节:如果 pipeline 里不同样本的img_shape不同,collate 会报错,所以训练时配RandomResize这类变尺度增强时,必须保证最终Pad到同一尺寸,或者使用支持动态 shape 的 collate 策略。

在实际项目中,我经常看到有人把batch_size调大之后显存爆炸,就单纯以为是模型太大。其实有可能是图片 padding 之后,很多空白区域也在算梯度,白白浪费显存。优化数据侧往往比换小模型更有效。

5. 接入一套全新数据集:最小改动原则

接新数据集是大家问得最多的问题。我的经验是:MMDetection 官方有CustomDataset,但最快路径其实是把你的数据转成 COCO 格式,然后只改配置,不改代码。

5.1 从零准备你的标注文件

COCO 格式的 json 长这样:

{ "images": [ {"file_name": "000001.jpg", "height": 480, "width": 640, "id": 1} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 1, "bbox": [100, 100, 200, 300], "area": 60000, "iscrowd": 0} ], "categories": [ {"id": 1, "name": "cat"} ] }

这里 bbox 是[x, y, w, h]格式,和框架内部使用的 xyxy 不同。不用担心,框架在加载时会自动转成 xyxy。你要保证的是:category_id从 1 开始,image_id要能对应上images里的 id。

如果你手里是别的格式(VOC、LabelImg 的 xml、或者某个行业软件的导出格式),最快的办法是写一个几十行的脚本转成 COCO。我见过有人手写了一套 CustomDataset 子类来兼容自有格式,结果因为字段命名、类型匹配问题调了三天,回过头来发现转格式半小时就搞定了。

5.2 配置文件中真正会改的只有这几处

跑通一套新数据集的配置,核心就是改四个地方:

dataset_type = 'CocoDataset' data_root = 'data/my_dataset/' metainfo = dict(classes=('cat', 'dog')) train_dataloader = dict( dataset=dict( type=dataset_type, data_root=data_root, data_prefix=dict(img='images/'), ann_file='annotations/train.json', metainfo=metainfo, filter_empty_gt=True, pipeline=train_pipeline ) )

classes的顺序要和 json 里categories的 id 对应。比如你的数据集只有两类,类别 id 是 1 和 2,那么classes=('cat', 'dog')就对了。如果类别 id 是 0 和 1,你得在 json 里先调成 1 和 2,或者在代码里统一加 1。

模型头部(比如RetinaNetHead)的num_classes会自动根据 metainfo 里的类别数计算,这也是 MMDetection 比较贴心的地方,不用你手动去改 head 的输出了。

5.3 新数据集接入的三个大坑

第一个坑是图片格式。MMDetection 默认用 cv2 读图,BMP、PNG、JPEG 都支持,但对某些少见格式支持不好。我遇到过一个项目,图片是 tiff 格式,cv2 能读但通道顺序和色彩空间全乱,后来统一转成 PNG 才消停。建议所有图片先统一成 JPEG 或 PNG,文件名统一成数字编号,省得后面一堆麻烦。

第二个坑是空标注。很多自采数据集里混着大量“拍了一张但没标任何目标”的图片。这类图在 COCO json 里就是没有对应 annotation 记录。如果filter_empty_gt=False,训练时模型拿到的全是负样本,loss 会波动得厉害,严重时直接发散。我的建议是训练前先跑一个脚本统计一下空标注比例,超过 5% 就要谨慎处理,要么过滤,要么加入负样本策略。

第三个坑是类别不平衡。如果你标注了三万张图,其中两万九千张只有类别 A,一千张才有类别 B,那么模型会疯狂偏向 A。数据侧能做的补救之一是重采样,让每个 batch 尽量包含各类别样本。MMDetection 里可以通过配置 sampler 的样本比例来实现,但最简单的方式还是在数据准备阶段就做好类别均衡,别把压力全丢给模型。

6. 数据输入的常见问题与排查技巧

数据输入的问题,报错信息往往很抽象,什么IndexError、KeyError、AssertionError,看着都像代码 bug,实际上八成是数据本身的问题。这一节把我在几个项目里踩过、也帮别人解决过的典型问题整理成速查,你遇到了可以直接照方抓药。

6.1 调试数据链的“三板斧”

遇到数据问题,别急着看模型、改代码,先把数据链单独拎出来测。

第一板斧:可视化。把DetVisualizer用起来,把 ground truth 画在图上。一张图能看出 bbox 是不是跑到界外了、类别对不对、坐标单位是像素还是归一化值。很多人纠结“我是不是该写个自定义 transform 来做 Mosaic”,我说先别,先看看手头数据长啥样再说。

dataset = build_dataset(cfg.train_dataloader.dataset) data = dataset[0] img = data['inputs'].numpy().transpose(1, 2, 0) # CHW -> HWC

第二板斧:单样本 pipeline 追踪。单独实例化一个 pipeline,然后打印每一步的输出 key 和 shape,能看到数据在哪个环节“变质”了。这招在定位 KeyError 的时候特别好用,你只要比对两个相邻 transform 之间的差异,就能知道是谁往 dict 里塞了不该有的东西、或者删了不该删的 key。

第三板斧:固定随机种子复现单样本。pipeline 里的RandomFlip、RandomCrop都有随机性,定位问题的时候先把seed固定,或者临时把概率设成 0,保证问题能稳定复现。随机性问题最烦人,不固定的问题根本没法查。

6.2 高频问题速查表

现象可能原因排查办法
KeyError: 'img'LoadImageFromFile 没执行,或路径错误打印 data_info['img_path'],检查文件是否存在
bbox 出现负值Resize 的 bbox_clip_border=False改回 True,或在 pipeline 里加 FilterBox 类过滤
训练 loss 为 nan数据里有非法标注或图片损坏检查是否有空标注、bbox 全零、图片全黑
加载很慢,GPU 利用率低num_workers 太小,或磁盘 IO 瓶颈调大 num_workers,或把图片预处理成 lmdb/缓存
模型预测结果全是背景类别编号从 0 开始,和背景冲突检查 metainfo 和 json 的 category_id 对齐情况
两个 epoch 之间结果差异大shuffle 的随机种子变化导致的正常波动固定 seed,或调整 num_workers 提升稳定性

这里面最容易被忽略的是“图片全黑”。见过一个案例,某工业质检项目里有一批图是暗光环境拍的,肉眼看着有内容,但直方图几乎压在最左侧,归一化之后信息量趋近于零,模型训练出来精度低得离谱。这类问题一般通过batch_sampler做质量过滤,或者在 data pipeline 里加MinIoUCrop、RandomBrightnessContrast来缓解光照影响。

6.3 我在数据输入上的几点心得

做目标检测这几年,如果只让我说一条经验,那就是:绝大多数训练事故都出在数据侧,而不是模型侧。与其盯着 loss 曲线猜模型哪里出了问题,不如先把数据链路可视化、逐层撸一遍。数据没问题,模型的问题才值得花时间。

另一个习惯是:新项目先跑通最小配置,再做增强。很多人一上来就把 mosaic、mixup、多尺度全开了,结果出了问题根本不知道是哪一步引起的。我自己的流程是先只用 Load + Resize + Flip 跑到收敛,再逐步叠加增强项,每加一项就重新评估一次。这样即使某一步引入的副作用,也能快速定位。

最后再分享一个小技巧:在 pipeline 里临时加一个Print类打印关键字段,比断点调试更省事,尤其在多进程 DataLoader 环境下,断点经常会跳来跳去。数据输入这块,宁可多花半小时做可视化确认,也别在训练跑了一半之后才发现标注格式有问题。基础打好了,后面的工程化路才会平坦。

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

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

立即咨询