1. 项目全景与核心思路
1.1 从实例分割说起:Mask R-CNN在解决什么
Mask R-CNN这个名字,做了一段时间计算机视觉方向的人应该都不陌生。我第一次正经用这个模型,是在一个航拍建筑物提取的项目里。项目拿到的数据是几千张无人机拍的可见光图,需要把每一栋房子的轮廓抠出来,不仅要知道"哪个位置有房子",还要知道"房子在像素级别上占了哪一片区域"。这种既做目标检测、又做像素级分割的需求,正是实例分割(Instance Segmentation)的典型场景,而Mask R-CNN就是这一方向最具代表性的成熟方案。
很多刚接触Mask R-CNN的人,会把它的能力和语义分割搞混。语义分割做的事情是把图像里每个像素分类,比如"路、树、建筑、人"各归各的标签,但同一类的多个独立物体会被混成一个整体;实例分割则要求把每一个独立个体单独区分出来——图像里有三栋楼,就要输出三块分开的掩码,而不是一大片"建筑"标签。Mask R-CNN在目标检测框架上多扩展了一个掩码分支,因此同时具备检测框、类别、逐像素掩码三种输出能力。
这个项目标题里最有价值的关键词其实是"自己的数据集"。模型本身是现成的,真正有区分度、也真正花时间的,是如何把你手头收集到的图像转换成一个模型能理解的训练集。很多教程喜欢用COCO、PASCAL VOC这类现成数据跑通流程,可项目落地时数据和标注通常都是自己的,格式、类别、目录结构全要自己搞定,这一环往往是新手翻车最多的地方。
1.2 为什么选Mask R-CNN:选型逻辑与场景匹配
我不止一次被问到"现在有那么多新模型,为什么还要用Mask R-CNN"。说实话,它在推理速度上确实不如YOLO系列里新出的分割版本(比如YOLOv8-Seg),结构也更重,但它的优势在别处。
首先,Mask R-CNN的资料完整度极高。从两阶段的Faster R-CNN架构,到RoIAlign、FPN(特征金字塔网络)、掩码分支的损失设计,每一步都有大量开源实现和博客深入解读。对你来说,遇到问题能搜到答案,这一点在工程开发里非常重要。
其次是精度,尤其是在目标形状复杂、相互遮挡多、实例密集的场景下,两阶段模型的候选区域精修机制让它比单阶段模型更稳。我做过一个工业零件计数项目,工人们随手摆在托盘上的零件,有旋转、有堆叠、还有不规则边缘,这种情况下Mask R-CNN的效果明显好于一次回归到位的单阶段方案。
选型还需要考虑你自己的数据规模。如果你的类别非常少、背景变化不大,比如只需要分割"圆孔"和"凹槽"两种对象,那一个轻量级模型或者语义分割模型就够用;如果你的目标类别多、场景复杂、且对分割边界精细度有要求,Mask R-CNN是性价比最高的起步选择。我们用它的那次,实际类别不超过十类,但形状种类多、环境光线变化大,Mask R-CNN跑出来的掩码边缘能稳定贴合建筑屋檐线,这套稳定边界质量在后端计算面积时省了不少事。
1.3 完整链路与知识准备
"使用Mask R-CNN训练自己的数据集"表面上看是一句需求,实际上背后是一整条链路:图像采集与清洗、多边形标注、标注格式转换、框架安装、数据集注册、网络配置、迁移学习、训练监控、指标评估、模型导出部署。任何一步出问题,都会卡住整个进度。
所以动手之前,先把下面这些基础概念理一遍:目标检测里的Anchor与候选区域、边界框回归与非极大值抑制;语义分割与实例分割的区别;COCO数据集的JSON标注结构;以及迁移学习的基本思路——也就是利用在COCO上预训练好的权重来初始化模型,再用自己的小数据集微调。不需要全部精通,但至少要理解这些名词在训练流程里负责哪一环,后面看报错日志时才不会两眼一抹黑。
还有一个容易被忽略的准备,是硬件条件。训练Mask R-CNN对显存有一定要求,常用的ResNet50+FPN骨干,batch size设2,一张图分辨率在1000像素上下,8GB显存就已经很紧张了;想跑得舒服,至少建议12GB以上显存的显卡。不满足也没关系,后面我会讲怎么通过混合精度、梯度累积这些小技巧在有限显存下把训练撑起来。
2. 数据集准备:最容易翻车的环节
2.1 标注工具的选型与实操要点
数据标注是这条链路里最吃力不讨好的环节,但它的质量直接决定最终模型的AP值下限。标注工具我先后试过很多种,最终常用的是LabelMe和CVAT。
LabelMe的优势是单机就能用,浏览器打开,多边形标注操作非常顺手,导出的JSON结果也干净,适合个人和几十人以内的小团队。CVAT则是更强的团队协作工具,带在线标注、多人任务分配、自动保存,适合样本量过万的批量项目。还有一套极简方案叫VIA,不需要安装任何东西,打开网页就能标,适合快速验证几百张图的流程跑通。
标注操作本身要注意的关键点有三个。第一个是沿物体边缘取点时,尽量贴着轮廓内侧而不是外侧走。取点偏离半个像素肉眼看不出来,但在模型学习时会形成一整套偏大的掩码误差。第二个是多边形顶点数要适度,光滑曲线用8到12个点就能表达得很好,没必要密密麻麻取上百个点,反而会给转换后的坐标增加无用噪声。第三个是特殊形状的处理——有镂空的物体(比如甜甜圈形状),很多初学者会画一个外轮廓就完了,结果把中间洞也标成了目标,正确做法是用多边形组把外边界和内边界分开表示。
如果项目有遮挡目标,你还要想清楚一个哲学问题:看不见的部分要不要标?标了会让模型学习到"完整形状被遮挡时的样子",不标则会让模型以为"目标天然有缺口"。我的习惯是,遮挡面积超过一半的目标直接不标,只标完整可见或遮挡轻微的实例,这样训练出来掩码更干净。
2.2 从标注结果到COCO格式:细节决定成败
标注工具导出的通常是一串多边形坐标JSON,但Mask R-CNN主流实现接受的训练格式是COCO JSON。如果你所在领域已经有某个数据结构,比如遥感方向常见的GeoJSON,也需要先转换成COCO格式。这一步绝大多数同学都会遇到格式兼容问题,我在这里把COCO标注最核心的字段拆开讲清楚。
一个标准的COCO标注JSON由info、licenses、images、annotations、categories五部分构成。images数组里每张图有一个唯一的id,对应图片文件名和宽高;categories数组里每个类别也必须有唯一的id,并且注意id从1开始,类别名称保持英文或拼音,尽量不要用中文;annotations数组则是逐条实例标注,常见字段如下:
{ "id": 1, # 本标注实例的编号,全局递增 "image_id": 1, # 对应images里的id "category_id": 1, # 对应categories里的id "bbox": [x, y, w, h], # 实例外接框,浮点数 "area": 1000.5, # 多边形面积,用于计算mAP时的面积统计 "iscrowd": 0, # 是否密集人群/目标,一般写0 "segmentation": [ [x1, y1, x2, y2, ...] # 多边形顶点坐标,一维数组展开 ] }很多人第一次转换时会在segmentation上卡住。LabelMe输出的每个点是[x, y]对,但COCO格式要求把坐标展开成一维数组存入——也就是变成[x1, y1, x2, y2, ...]。另外,如果你的实例是镂空的,segmentation需要写成多个多边形数组,不能用单条数组表达,否则模型训练时一旦采样到内轮廓也会把它当实际内容。
area字段同样值得留意,虽然它看起来只是用于聚合统计,但训练时很多框架会按面积区间对检测结果做分类统计,如果换算错了会导致小目标AP异常偏低。
2.3 数据集划分、类别ID与自查脚本
数据准备完成后,必须做两次自查。第一次检查的是格式合法性,第二次检查的是视觉正确性。
格式合法性最简单的办法,是直接把JSON加载进来跑一遍基础断言:所有image_id都能在images表里找到;标注坐标数组的长度必须为偶数;bbox的x、y不能为负数,且x + w不能超出图像宽度;所有category_id都存在于categories字典。这些条件如果有一个不满足,训练时轻则报警告,重则进程直接崩溃。
视觉正确性自查要靠可视化脚本。我强烈建议你在写转换脚本时,顺便写一个小的可视化函数:把每张图的检测框和掩码边界绘制在原图上保存成一张新图,然后肉眼抽查200张。我见过最离谱的错误是某个同学坐标转换时少乘了一个原始图像缩放比,导致所有标注都只有真实物体大小的四分之一,如果用脚本画出来一眼就能发现,但他没检查就直接开训,loss下降后预测效果却一团糟,回头排查花了两天。
数据划分上,常见比例是train:val:test = 7:2:1。需要注意划分的随机性,以及尽量保证每个类别在各个集合里的分布比例大致一致。如果某个类别在训练集中只出现在两成图像里,模型对这个类别的学习会明显吃力。
3. 环境搭建与源码改造
3.1 框架选型:Detectron2与MMDetection怎么选
拿到标注数据之后,接下来要选具体的实现框架。主流的选择有两个:Facebook出品的Detectron2和OpenMMLab体系的MMDetection。两个框架我都深度用过,说下真实感受。
Detectron2的优点是上手轻快,安装简单,自定义数据集注册只需要三行API,日志和可视化工具完善,对我们这种"想快速验证数据流程是否正确"的场景非常友好。它的Mask R-CNN实现也稳定,社区答疑多。缺点是它的配置系统比较"重",很多训练细节被封在DefaultTrainer里,懂原理的人想改底层逻辑反而要翻不少源码。
MMDetection的优势是模型库极其丰富,同门分出去的分割工具MMSegmentation可以无缝衔接,但配置文件和依赖链相对复杂,对新手不够友好。如果只是在Mask R-CNN一条路上解决问题,我更推荐Detectron2——它在安装和数据集注册上省下的时间,足够你进行三次完整的数据自查。
版本上,Detectron2建议直接安装最新稳定版,它依赖PyTorch,所以先装好CUDA版的PyTorch再装Detectron2,顺序不要反过来。
3.2 注册自定义数据集:最小可跑通代码
Detectron2的数据集注册,核心是提供一个注册函数和一个返回标注字典列表的函数。这里我贴一套我在两个项目里都验证过的最小代码:
from detectron2.data import DatasetCatalog, MetadataCatalog def get_my_dataset_dicts(): # 读取cooc格式json,转成detectron2需要的数据格式 dataset_dicts = [] with open("annotations/instances_train.json", "r") as f: coco_data = json.load(f) img_id_to_info = {img["id"]: img for img in coco_data["images"]} cat_id_to_name = {c["id"]: c["name"] for c in coco_data["categories"]} for ann in coco_data["annotations"]: image_id = ann["image_id"] img_info = img_id_to_info[image_id] record = {} record["file_name"] = os.path.join("images/train", img_info["file_name"]) record["image_id"] = image_id record["height"] = img_info["height"] record["width"] = img_info["width"] # 将每个标注实例组织为detectron2的annotations格式 record["annotations"] = [] # ... 这里按需填充segmentation、bbox、category_id等字段 dataset_dicts.append(record) return dataset_dicts DatasetCatalog.register("my_train", get_my_dataset_dicts) MetadataCatalog.get("my_train").set( thing_classes=["building", "vehicle", "bridge"], evaluator_type="coco" )注意这里有个很容易绕晕的点:COCO的annotations里每条记录是"一个实例",而Detectron2的dataset dict是按图像组织的,每张图对应一条record,里面挂一个annotations列表。转换时一定要分组聚合,否则同一张图被拆成多条record,训练时等价于同一个文件被重复加载多次,而且标注会错位。
注册完成后,先用官方的可视化脚本把visualizer.draw_dataset_dict()跑一遍,确认你注册的数据确认无误。这一步当作流程的第三道自查,别嫌麻烦。
3.3 网络结构与训练配置调整
Detectron2里Mask R-CNN的配置可以通过修改一个YAML文件完成。如果你直接改官方提供的mask_rcnn_R_50_FPN_3x.yaml,需要关注的核心配置项是:
MODEL: MASK_ON: True # 关键:你的类别数 = 背景 + 真实类别数 ROI_HEADS: NUM_CLASSES: 4 # 输入图像缩放,影响显存和精度 INPUT: MIN_SIZE_TRAIN: (800, 896) MAX_SIZE_TRAIN: 1333 OPTIMIZER: BASE_LR: 0.001 SOLVER: IMS_PER_BATCH: 4 STEPS: (24000, 32000) MAX_ITER: 40000NUM_CLASSES这项最容易写错。很多人误以为它填真实类别数就行,但Detectron2在做分类时会把背景当成额外的一个类别,所以这个数字应该是"真实类别数+1"。假设你分建筑、车辆、桥梁三类,这里应填4。填成3的话,训练不会报错,但推理时最后一个类别的输出会错分到背景,AP直接掉一半。
骨干网络的选择上,ResNet50+FPN是性价比最高的默认配置,显存要求适中,精度和速度平衡好;如果你有充足算力且目标较小,可以升级到ResNet101+FPN。训练策略则建议从3x(表示在COCO上约37个epoch)模式开始,它内部已经包含了多尺度训练和合理的learning rate schedule,比自己手搓一个1x配置更省心。
4. 训练全流程与参数调优实战
4.1 迁移学习的正确打开方式
你自己的数据集很可能只有几百上千张图,从随机初始化开始训练一个几十层的ResNet,效果会非常差。所以几乎所有人都会加载在COCO数据集上预训练好的权重。
Detectron2里做这个操作很简单,配置文件中设置MODEL.WEIGHTS为预训练权重路径即可。
wget https://dl.fbaipublicfiles.com/detectron2/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl注意下载地址里要对应你配置文件里的backbone类型,R50和R101的预训练权重不通用。加载时Detectron2会做一次权重匹配检查,如果headers层出现了维数不匹配的警告,多半是你改动了NUM_CLASSES导致的——这个警告正常,因为分类头原来的输出维数对应80个COCO类别,你的数据集类别数不同,头部自然没法完全继承。
如果没有做迁移学习就裸训练,你会发现训练曲线迟迟上不去,往往要到两三万个iteration之后才出现可用的掩码,而且边界质量远远不如预训练权重微调出来的效果。我在实际项目里做过对比,同样的数据集,预训练方式在第2000个iteration时就已经能看到可以接受的轮廓,而随机初始化到第20000个iteration时依然存在漏检和错检。
4.2 超参参考表与调整逻辑
先上一张我自己整理的参考表,单卡训练时直接照抄比较稳:
| 参数 | 参考值 | 说明 |
|---|---|---|
| BASE_LR | 0.001(batch=4时) | 学习率与batch size成正比调整,batch翻倍时LR也翻倍 |
| IMS_PER_BATCH | 2 ~ 8 | 单卡8G建议2,16G可以4,企业级24G卡可以到8 |
| MAX_ITER | 20000 ~ 50000 | 先按数据量估,小数据集中途看loss决定是否停 |
| STEPS | MAX_ITER的0.6和0.85处 | 学习率下降节点 |
| WARMUP_ITERS | 1000 | 前1000步线性从0升到设定LR,避免开局震荡 |
| MIN_SIZE_TRAIN | (800, 896) | 多尺度小幅抖动,正样本丰富度提升明显 |
| FPN_NORM | GN | 数据量小时GroupNorm比BN稳定,不会因为batch小而爆炸 |
学习率的选择逻辑不是拍脑袋:如果你把batch size从4降到2,那么LR也要相应减半到0.0005,否则梯度累积步数更多,而单步参数更新量不减,loss曲线会剧烈震荡。有个好用的经验是观察loss曲线——完全见不到下降趋势,首先去看是不是LR太小;loss像锯齿一样疯狂跳动,则多半是LR背景了。
还有一点是关于ROI_HEADS.SCORE_THRESH_TEST,这个值只影响推理时不显示低置信度框,不影响训练时内部的正负样本均衡,新手容易在这里产生困惑。
4.3 训练过程监控:loss、速率与AP曲线
开训之后,你的日常就是盯日志和曲线。Detectron2的默认输出已经包含loss_cls、loss_box_reg、loss_mask、loss_rpn_cls、loss_rpn_loc几个分量,总loss是它们的加权和。其中loss_mask这个分量能否正常下降,是分割部分能否学会的关键信号。
训练速率的监控也很有价值。同一个模型,相同batch size下,如果两台机器的iteration耗时差异超过30%,先去看数据加载瓶颈——最常见的是磁盘IO慢或者worker数量设置不够。Detectron2中的NUM_WORKERS建议设为8到12,但这取决于CPU核数和数据读取方式,不要无脑拉高,进程调度开销有时会反而变慢。
最后配上每2000次iteration做一次验证,观察box AP和segm AP。我的习惯是验证时不只记录mAP数值,而是把验证集上预测出的掩码图保存下来,生成一个网页式的大图方便翻看。你才能直观看到哪些目标被漏掉、哪些轮廓不对,这比纯看数字更能定位问题。
5. 常见问题排查与避坑实录
5.1 高频报错与处理方案
我整理了一份排查表,记录的是实际项目中最高频的六个问题:
| 报错现象 | 根本原因 | 解决办法 |
|---|---|---|
| KeyError: 'file_name' | 数据集注册返回的dict字段不完整 | 检查dataset dicts是否包含file_name、height、width、image_id |
assertionx >= 0failed | bbox坐标出现负值 | 检查标注转换精度,裁掉越界坐标时别直接丢弃实例 |
| IndexError: index out of range | 类别ID超出了metadata里thing_classes的长度 | 检查categories中id是否从1开始且连续 |
| Expected input tensor has 4 dims, but got 3 | 某张图是灰度图或单通道 | 将所有图像统一转为三通道RGB再处理 |
| CUDA out of memory | 显存不足 | 减小batch size或MaxSize,开启混合精度 |
| loss为nan | 学习率过大或标注里有空标注 | 降低LR,检查annotations里是否有segmentation为空的实例 |
空标注问题值得特别提醒。我在某个项目里发现训练日志中loss_mask偶发nan,排查了一圈才发现是转换脚本处理了一张特殊图片——那张图上所有标注都被过滤掉了,image record里annotations为空。Detectron2对这种情况不会立即报错,而是在采样时随机选到一个空目标的RoI,计算出奇特的loss值。在注册数据集时加一行过滤,把annotations为空的record直接剔除,问题即可解决。
还有一个隐藏极深的问题,是缓存。Detectron2的数据集注册有本地缓存机制,如果你改了标注JSON但没换数据集名字,重新训练时可能仍在使用旧缓存。排查这类"明明改了数据但效果没变"的诡异问题,最简单的方法是注册名后面加版本号,比如"my_train_v2"。
5.2 显存与训练效率优化
当显存不够但又想训练大模型,优先级排序是:先开混合精度,再降batch size,再降最大输入分辨率,最后才考虑换小骨干网络。混合精度(AMP)在Detectron2中开启方式是:
SOLVER: AMP: ENABLED: True这块落地后显存占用能减少约30%,而且我们实测在Mask R-CNN任务上,精度损失通常在0.2个AP以内,完全可接受。
batch size降到2之后,全局梯度噪声变大,需要注意把学习率同步降下来。如果你连batch size为1都跑不下,只能降MAX_SIZE_TRAIN来控制单图尺寸,从1333降到1024,但这也意味着小目标的检测效果会折扣。
另一条路是多卡训练。DistributedDataParallel在Detectron2里几乎是开箱即用,一条命令加上--num-gpus 2就能跑起来。我遇到过不少同学,卡里显存明明只有8G,非要在单卡上硬跑batch 4,实际把batch降到2、实验周期反而缩短,因为不需要反复OOM重启了。
5.3 效果不理想时的调试顺序
如果模型训完但效果很差,我推荐按下面顺序排查,而不是一上来就调参:
第一,先看数据自查画出来的图,确认标注边界有没有明显错位、类别有没有标反。数据错的话,模型训练再久也白费。第二,看训练集损失和验证集损失之间的差距。训练损失正常下降但验证AP很低,说明过拟合了,考虑加数据增强或缩小模型。第三,检查是否类别不平衡。如果你的数据集里"桥梁"只有20个实例,"建筑"有2000个,模型当然会偏向建筑,可以通过重采样或者给少数类加权重来缓解。第四,是看漏检的目标类型。漏检集中在远处的小目标,说明输入分辨率不够;漏检集中在遮挡目标,说明标注策略有问题;漏检集中在光线暗的图,说明数据增强里缺少亮度扰动。
这套排查顺序帮我省下了大量试错时间,比起一上来就把学习率乱调一顿,还是先定位问题层级更有效。
6. 评估、推理与部署落地
6.1 评估指标解析与掩码可视化自查
训练完成后,模型到底行不行,不能只看Training loss降得多低。Detectron2内置的COCO evaluator会输出两组指标:box AP和segm AP。segm AP就是掩码的平均精度,是我们最关心的;AP还按大小分成了AP-small、AP-medium、AP-large,如果你的目标普遍偏小,而AP-small特别低,说明特征是细节提取不够,需要考虑更高分辨率的输入或更强backbone。
有一个容易被忽视的问题是掩码边界质量。AP数字高不代表掩码好看,因为AP的核心是匹配查全率和查准率,边界的亚像素精度它管不着。所以评估时一定要肉眼抽查结果,看这几个方面:掩码有没有裂缝或空洞、边界是否沿着目标边缘、相邻重叠目标是否被错误合并。这些是纯mAP体现不出来的。
遇到掩码总是漏掉目标某个部位的情况,可以试试调大训练时的迭代轮次,并配合减少ROI_HEADS.NMS_THRESH_TEST来保留更多候选框。但治本之策还是回到数据,去补充这些部位形态的样本。
6.2 模型导出与部署注意事项
训练好的模型要落地,通常不会继续在训练框架里跑推理。我这里讲两个层次的部署方案。
快速原型阶段,直接用Detectron2的DefaultPredictor,加载model_final.pth权重,简单封装一个Flask接口就能给业务方演示。要注意的是DefaultPredictor每次推理都做输入图像预处理,如果你有大量小图拼接请求,务必在外面做好缓存,否则图像缩放会成为性能瓶颈。
生产环境要求更高吞吐的场景,建议导出为ONNX或者TensorRT格式。Mask R-CNN导出比纯分类模型麻烦很多,因为它包含RPN阶段的动态Anchor选择和后续的RoIAlign操作,这些在静态图推理框架里不一定有完全对等的算子。避坑经验是先固定输入尺寸,再逐模块导出,输出端的检测框和后处理用原框架完成,这样能绕开大量算子兼容问题。我们当时接的是视频流,每秒25帧,就是采用"检测模块TensorRT化 + 追踪后处理单独跑"的组合方式,整体延迟控制在可控范围内。
部署后的监控也别忘了。我在项目上线后加了一个保护逻辑:当单帧检测到掩码面积总和连续N帧异常小时,自动重新加载备用模型权重。业务场景里数据分布会漂移,一套权重打天下是不现实的,提前做好模型更新通道比什么都强。
最后再分享一个我的个人习惯,训练任何自定义数据集时,我都会同步记录一份训练日志文件,不仅记loss、AP,还记录当天的数据版本、超参配置、遇到的报错和解决方式。出现过两次"明明这个参数以前调过,现在想不起来当时是怎么调的"之后,我就把这个习惯固化下来了。做Mask R-CNN的坑真的不少,但绝大多数都是前人在文档和Issue里踩过并留下痕迹的,养成记录习惯,你翻笔记的时代会比重新搜走过的时代省力很多。