1. 数据集准备:从原始图像到mmsegmentation能吃的格式
1.1 为什么说数据集格式是mmsegmentation的第一道坎
很多人拿到mmsegmentation第一件事就是clone仓库、装环境、跑demo,然后卡在“我想训练自己的数据”这一步。这是正常的,因为mmsegmentation不像detection系列那么好糊弄——分割任务的数据集格式严格得多,一个类是文件夹也好,一张图是一个png也好,都需要按它的约定来组织。社区里有个很经典的段子:在mmsegmentation里跑通cityscapes模型的成功率是90%,但用自己数据集跑通训练流程的成功率不到50%,剩下的50%基本都死在数据集格式上。
这里先说清楚一个基本概念:mmsegmentation底层依赖mmcv和mmengine,数据加载走的是CustomDataset这个基类。它默认要求你的数据是“一图一掩膜”的结构:原始图像是RGB三通道的jpg/png,对应的标签是单通道的png(8位或16位整数),像素值就是类别ID,从0开始计数,0通常作为背景类。这个设计沿用了PASCAL VOC的经验,但比VOC更严格——VOC的label是调色板PNG,mmsegmentation直接读取像素值。
所以拿到你自己的项目时,第一步不是写网络结构,而是回答三个问题:我的图像是什么格式?我的标注是什么格式?我的类别ID怎么映射?这三个问题没想明白前,改再多config都是空转。
1.2 VOC格式和MSTAR格式的取舍,我推荐哪一种
mmsegmentation的官方文档里提到了两种常见组织方式,一种叫VOC格式,一种叫MSTAR格式(其实是很多遥感、医疗项目里默认的简单格式)。我实际用下来,强烈建议新手直接走MSTAR风格,也就是纯文件夹结构:
data/ ├── imgs/ │ ├── train/ │ │ ├── 0001.jpg │ │ ├── 0002.jpg │ ├── val/ │ │ ├── 0001.jpg ├── masks/ │ ├── train/ │ │ ├── 0001.png │ │ ├── 0002.png │ ├── val/ │ │ ├── 0001.png它的好处是:目录结构一目了然,不需要写额外的splits文件,只需要在config里指定img_dir='imgs/train'、ann_dir='masks/train'就能跑。相比之下VOC格式还要维护train.txt、val.txt这样的索引文件,除非你有大量历史数据已经按VOC组织好了,否则没必要自找麻烦。
但要注意一个细节:MSTAR风格下,img和mask的文件名必须一一对应,也就是说imgs/train/0001.jpg的标签必须是masks/train/0001.png,多一张、少一张、后缀不一致,训练时都会报错或者产生标签错位。这种错位很隐蔽,因为mmengine的数据管道不会校验图像和标签是不是同一个场景,只会检查张量形状。我以前就吃过亏,一张翻车的mask混在数据集里,模型分割精度直接掉了两个点,还查了半天。
1.3 数据标注阶段的三个实用建议
如果你是从头标注一个新数据集,工具上首推LabelMe或者X-AnyLabeling。LabelMe是老牌选择,导出的是JSON多边形;X-AnyLabeling支持SAM辅助标注,速度会快很多,尤其做遥感或者医疗图像时,SAM的边界质量比纯手动高不少。但不管用什么工具,我有三个建议:
第一,先标10张,跑通整个训练流程,再回来批量标注。很多人一次性把几百张全标完,结果发现格式转换有问题、类别ID映射错了,全部返工,心态直接爆炸。先小批量验证格式,是控制风险最好的方式。
第二,标注的类别定义要和控制文件一致。比如你定义了“道路、建筑、车辆”三个前景类,那么像素值1、2、3分别对应这三个类,背景是0,不要用255做背景。mmsegmentation的ignore_index默认是255,可以用于标注那些“我不想参与损失计算”的区域,但不要和背景混淆。
第三,注意类别的边界问题。分割任务的本质是像素级分类,如果在标注时两个相邻物体的边界画得模棱两可,模型学到的边界就会模糊。LabelMe这类工具支持多边形吸附和顶点编辑,标注时尽量贴着物体轮廓走,宁可多花10秒钟把一个角标准了,也别图快画个大差不差的多边形。边界质量直接决定mIoU的上限。
1.4 手动转换label为单通道灰度图的操作细节
不管你用什么工具标注,最终都要转成单通道灰度PNG。以LabelMe导出的JSON为例,转换思路是:读取JSON里的形状和标签,按category列表建立ID映射,用PIL或OpenCV在多边形内填充对应的整数像素值。
import json import numpy as np import cv2 from pathlib import Path # category顺序很重要,下标就是像素值 categories = ['_background_', 'road', 'building', 'vehicle'] cls2id = {name: i for i, name in enumerate(categories)} def json2mask(json_path, img_shape=(512, 512)): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) mask = np.zeros(img_shape, dtype=np.uint8) for shape in data['shapes']: label = shape['label'] if label not in cls2id: print(f'警告:{label} 不在类别列表中,跳过') continue points = np.array(shape['points'], dtype=np.int32) cv2.fillPoly(mask, [points], cls2id[label]) return mask # 使用示例 mask = json2mask('label.json', (512, 512)) cv2.imwrite('mask.png', mask) # 注意:cv2.imwrite保存单通道png时,会被自动压缩为灰度图,是安全的这里有一个必须强调的坑:保存mask时,不要用matplotlib的imsave,它会自动加上colormap,存成三通道的伪彩色图;也不要用cv2.imwrite保存带调色板的PNG索引图以外的任何变体。最稳妥的方式是上面看到的,直接用OpenCV写uint8的单通道数组,文件后缀叫png就行。训练前务必做一次像素值分布检查:
import numpy as np mask = np.array(Image.open('mask.png')) print(np.unique(mask)) # 期望输出:[0 1 2 3],如果出现[0 1 255]这种,说明格式有问题2. Config文件修改:不复制粘贴,理解之后再动手
2.1 mmsegmentation的config体系是怎样分层的
mmsegmentation的config不是一个大而全的文件,而是分层组合的。拿pspnet_r50为例,它对应的config会继承一份自_base_目录下的四类基础配置:数据集配置、模型配置、训练策略配置、运行时配置。这个设计和mmdetection一脉相承,好处是公用部分不用重复写、不会被改乱;坏处是新手很难一眼看出“到底哪个文件决定了我当前的训练行为”。
我建议你拿到一个官方的cityscapes config之后,第一步先跑一下python tools/print_config.py 你的config.py,它会把继承展开之后的所有配置完整打印出来。这个命令非常重要,它能让你看到所有继承关系被解析后的完整config,避免你在层层继承里迷路。而且每次改过config后都值得重新打印一遍,确认改动生效了。
实际项目里,我不建议像官方那样搞太多层继承。最多两层就够了:一层是基础数据集配置,一层是你当前实验的专属配置。层级太多时,调一个学习率要打开三个文件,心智负担太重,还容易改错。
2.2 data配置:dataset_type、data_root、img_dir和ann_dir
config改动里最核心的是dataset_type和data这两个字段。先说dataset_type,很多博客会让你直接改成dataset_type = 'CustomDataset',这个方向是对的,但有个隐含条件:如果你的数据格式是“一图一掩膜”的文件夹结构,并且类别数不与任何内置数据集一致,那么必须用CustomDataset。同时你还需要指定一个metainfo字典,把类名列表和类别颜色告诉框架,否则mmsegmentation会从默认的cityscapes类名里取,训练时类别名称会乱套。
dataset_type = 'CustomDataset' data_root = 'data/mydata/' metainfo = { 'classes': ('background', 'road', 'building', 'vehicle'), 'palette': [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0]] } train_dataloader = dict( batch_size=4, num_workers=4, persistent_workers=True, sampler=dict(type='InfiniteSampler', shuffle=True), dataset=dict( type=dataset_type, data_root=data_root, data_prefix=dict(img_path='imgs/train', seg_map_path='masks/train'), metainfo=metainfo, pipeline=train_pipeline) )注意这里的data_prefix字段,官方新版用了dict(img_path=..., seg_map_path=...),旧版本的代码里可能叫img_dir和ann_dir。这两个写法都算合法,取决于你的mmsegmentation版本,2.x系列建议用data_prefix。如果遇到数据加载为空的报错,第一排查项就是data_prefix里的目录名和磁盘上的实际目录名是否完全一致。
还有一个高频问题:classes里的元组顺序必须和标注时定义的类别ID一一对应。你标注时像素值1是road,config里classes元组的第一个位置就必须是road;如果有错位,模型训练时不会报错,但预测结果的语义就全乱了,评估时的mIoU也会很难看。
2.3 model配置:num_classes和in_channels必须改对
在model字段里,最容易漏改的就是decode_head和auxiliary_head里的num_classes。你用的任何官方config,默认值都是cityscapes的19类或者ade20k的150类,不改这个参数直接训练自己的数据,框架会在构建模型时直接抛“inconsistent”之类的报错。
具体操作很简单,在config文件里加一行覆盖:
model = dict( type='EncoderDecoder', data_preprocessor=dict( type='SegDataPreProcessor', mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], bgr_to_rgb=True, pad_val=0, seg_pad_val=255), backbone=dict( type='ResNetV1c', depth=50, num_stages=4, out_indices=(0, 1, 2, 3), dilations=(1, 1, 2, 4), strides=(1, 2, 1, 1), norm_cfg=dict(type='SyncBN', requires_grad=True), norm_eval=False, style='pytorch', contract_dilation=True), decode_head=dict( type='PSPHead', in_channels=2048, in_index=3, pool_scales=(1, 2, 3, 6), channels=512, dropout_ratio=0.1, num_classes=4, # 这里必须改 norm_cfg=dict(type='SyncBN', requires_grad=True), align_corners=False, loss_decode=dict( type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0)), auxiliary_head=dict( type='FCNHead', in_channels=1024, in_index=2, channels=256, num_convs=1, concat_input=False, dropout_ratio=0.1, num_classes=4, # 这里也必须改 norm_cfg=dict(type='SyncBN', requires_grad=True), align_corners=False, loss_decode=dict( type='CrossEntropyLoss', use_sigmoid=False, loss_weight=0.4)), train_cfg=dict(), test_cfg=dict(mode='whole'))同时要看一看backbone的in_channels。你的输入如果是普通RGB三通道图,保持默认3不用动;如果是灰度图、四通道遥感图或者带深度的图,就要改backbone里第一个卷积层的输入通道数。改法也不是直接改backbone的in_channels=1就完事,因为ResNet的第一个卷积权重是预训练好的,通道数变了,权重没法直接加载。常规做法是把预训练权重的第一个卷积层做平均,把3通道的权重复制到1通道,或者干脆不加载预训练权重、从头训练。这里提醒一下:除非数据量很大,不然从零训练ResNet50做分割,效果大概率不如用ImageNet预训练。所以数据如果不是特殊模态,建议直接三通道输入。
2.4 pipeline里的归一化参数和尺寸设置为什么不建议照搬
还有一个很容易被忽视的地方:data_preprocessor里的mean和std,官方config里默认是ImageNet统计量,即mean=[123.675, 116.28, 103.53]、std=[58.395, 57.12, 57.375]。
很多人拿着自己的数据集,尤其是在自然光条件下拍的可见光图像,会纠结要不要改成自己数据集的统计量。我的建议是:不要改。ImageNet的mean和std已经是一个非常好的分布近似,改不改对你的最终精度影响在0.5个点以内,而且改了之后如果你要用官方预训练权重,流程会更麻烦。说白了,这个参数只要保证训练和推理时一致就行,不是追求数据分布匹配的场合。
尺寸设置方面,train_pipeline里的Resize、RandomFlip、PhotoMetricDistortion都可以保留,但RandomCrop里的crop_size要根据你的显存来。比如你显卡只有11GB,crop_size设成(769, 769)对PSPNet来说非常吃力,改成(512, 512)才是务实的选择。val和test里的Resize一般设置成(512, 512)或者原图缩放到指定比例,注意保持和训练尺寸不要差太多,否则精度会波动。
3. 训练实战:从命令行到日志解读
3.1 训练前的一个关键测试:单步跑通前向
不要一上来就跑完整训练,先用一张图、一个batch、一个step验证流程。启动训练之前,我建议先跑一次完整的单步前向和反向:
python tools/train.py configs/pspnet/pspnet_r50_8xb2-40k_cityscapes-512x512.py --work-dir work_dirs/test_run如果正常启动,你会看到模型结构、参数量、dataloader信息。这个过程也顺便验证了config里的路径都能被正确解析。我第一次跑自定义数据集时少写了一个data_prefix路径,程序没报错,但每个batch拿到的都是空数据,训练loss一直是nan,折腾了很久才定位到问题根源。
还有一个更快的验证方式,写一个小脚本直接加载数据管道:
from mmseg.datasets import build_dataset from mmengine.config import Config cfg = Config.fromfile('my_config.py') dataset = build_dataset(cfg.train_dataloader.dataset) for i in range(3): data = dataset[i] print(data['inputs'].shape, data['data_samples'][0].gt_sem_seg.data.shape)只要能正常打出图像和mask的形状,且mask里的类别数是符合预期的,说明数据侧基本没问题,接下来训练就顺畅很多。
3.2 训练命令详解:单卡、多卡、断点续训和定时保存
正式训练的命令并不复杂,复杂的是根据自己的资源和需求组合参数。单卡训练最简单的写法:
CUDA_VISIBLE_DEVICES=0 python tools/train.py my_config.py --work-dir work_dirs/my_experiment多卡训练用官方推荐的分布式启动器:
bash tools/dist_train.sh my_config.py 4 --work-dir work_dirs/my_experiment后面的数字是GPU卡数。多卡时注意,如果你的config里没有设置sampler=dict(type='InfiniteSampler', shuffle=True),mmengine的DistributedSampler会自动处理数据切分,不需要手动管shuffle。
训练过程中最常用的两个参数是--resume和--cfg-options。前者用于断点续训,比如:
python tools/train.py my_config.py --resume work_dirs/my_experiment/iter_10000.pth后者用于临时覆盖config参数,这在跑消融实验时非常高效,不需要改文件:
python tools/train.py my_config.py --cfg-options optimizer.lr=0.01 model.decode_head.num_classes=4我一般会额外设置default_hooks.checkpoint.interval=2000,也就是每2000个迭代存一次权重。默认值是4000,如果中途崩了,最多可能丢4000个迭代的进度,对长时间训练来说不太划算。
3.3 训练过程中必须盯着哪些指标:loss为什么是nan,mIoU为什么不动
训练日志里至少要看四个东西:loss、decode.loss_ce、decode.acc_seg、data_time。前三个是模型收敛的直接体现,第四个能看出dataloader是不是瓶颈。如果data_time占了每步时间的60%以上,说明数据加载(或者数据增强)太慢,训练在空转,建议调高num_workers、检查磁盘IO,或把persistent_workers设为True。
loss变成nan是很多人第一次训练自定义数据集时最崩溃的事。原因不外乎三类:标签里有超出num_classes范围的像素值(最常见的,用的mask不是从0开始编号)、学习率过大导致梯度爆炸、或者图像里有损坏的文件。排查顺序也是这个顺序:先用上一节提到的脚本统计mask的像素值分布,然后把学习率降到默认值的1/10试跑一版,最后排除数据管道里混入的异常图。这三步能解决90%的nan问题。
如果loss正常下降但mIoU一直很低,比如低于0.5,多半是类别不平衡造成的。你的数据里如果背景像素占了90%以上,模型会趋向于把所有像素都预测为背景,mIoU自然上不去。这时候有两个调整方向:一是给loss加上class_weight,提高小类别权重;二是在pipeline里加RandomCrop,尽量让每个crop里的小目标占比更高。从我的经验看,class_weight对语义分割的提升效率没有检测任务里那么明显,优先考虑数据层面的均衡更有效。
3.4 两份官方热词带来的启发:远程开发时config文件的修改链路
顺着热搜词里提到的“vscode 远程config文件”往下说,我猜不少人是在远程服务器上开发,用VSCode Remote SSH打开项目。这种情况下,改config文件有个额外风险:本地和服务器上mmsegmentation的版本可能不一致,本地改了文件后不会自动同步,配置跑不起来时先确认改动是否真正到了服务器上。
我的习惯是项目里统一用相对路径写data_root,不写绝对路径,这样换机器时不用改config。还建议在config文件头部固定写好环境说明注释:
# 环境:A100单卡,CUDA 11.7,mmseg 1.2.0 # 数据集:自定义道路分割,4类(background, road, building, vehicle) # 预训练权重:resnet50_v1c.pth,自动下载自OpenMMLab官网这个注释在半年后回看实验时太有用了。你永远会忘记当时用了哪个版本、哪个数据集、哪个预处理,但注释不会。
3.5 训练结束后的权重保存和转出onnx的注意事项
训练结束后,work_dirs里会有iter_xxxx.pth或者epoch_xx.pth的权重文件。这些权重是mmengine的格式化dict,里面除了模型参数还有optimizer状态和meta信息,不能直接在原生PyTorch里load。如果你需要导出到onnx做部署,可以用官方脚本:
python tools/deploy/onnx_export.py my_config.py work_dirs/iter_10000.pth out.onnx如果只是想在本地用mmseg的推理API做效果验证,更简单的做法是:
from mmseg.apis import init_model, inference_model model = init_model('my_config.py', 'work_dirs/iter_10000.pth', device='cuda:0') result = inference_model(model, 'test.jpg') # result.pred_sem_seg.data 是HW的类别ID矩阵,直接用调色板映射成彩色图即可这个流程能快速让你看到分割效果,方便判断模型质量是否值得继续训练。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
下面这些问题是社群和评论区里翻来覆去地出现的高频问题,我整理成了一张速查表,遇到报错先对着排查一遍,大概率能省下半天时间。
| 现象 | 大概率原因 | 处理办法 |
|---|---|---|
训练启动时报FileNotFoundError | data_root路径或data_prefix目录名拼写错误 | 打开config,逐个核对目录 |
| loss为nan | mask像素值超出类别范围 | 用np.unique统计mask,保证最大值 = num_classes - 1 |
| 训练正常但mIoU极低 | 类别不平衡或标签错位 | 统计各类别像素占比,调整crop策略标签错位时检查img和mask文件名对应关系,mask尺寸与img不一致也会导致错位 |
| CUDA out of memory | crop_size过大或batch_size过大 | 调小crop_size到512或更小,batch_size降到2 |
| 加载预训练权重报错 | backbone in_channels与权重不匹配 | 检查in_channels是否为3,二/四通道数据需要自行处理权重 |
| 训练速度越来越慢 | 验证阶段加载了太多缓存,或pipeline里有耗时操作 | 查看data_time占比,调大num_workers,减少验证频率 |
有警告class_names not saved | 自定义数据集没有传metainfo | 在dataset字段里补上metainfo字典 |
| 推理结果全是背景类 | num_classes没改,模型输出维度与类别数不匹配 | 检查decode_head和auxiliary_head的num_classes |
| 可视化保存的mask是全黑的 | 用PIL保存时误用L模式但值范围超出显示范围 | 可视化时手动乘回255或使用调色板映射 |
4.2 我踩过的三个隐蔽坑
第一个坑是mask的通道数。有的工具导出的PNG虽然是.png后缀,但实际是4通道RGBA,直接喂给mmsegmentation会在损失计算时爆维度错误。处理方法是在数据预处理前统一用OpenCV读一次,检查mask.shape[2]是否存在,发现是三通道或四通道就做一次cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY)。类似地,有些标注工具会导出16位深度图,mmsegmentation也能处理,但如果你中间不小心转成了8位而且丢失了类别ID,那就彻底找不回来了。
第二个坑是训练和验证时图像尺寸不一致。我在一个项目里训练用512x512随机裁剪,验证时直接test_cfg=dict(mode='whole'),结果遇到一张2000x3000的大图,推理时显存直接不够用。后来用的是mode='slide',设置stride=256,虽然速度慢一点,但显存被稳定控制在可接受范围。这个细节在部署阶段尤其重要。
第三个坑是类别不平衡但靠loss_weight救场失败。实验做完后分析发现,问题出在CrossEntropyLoss的默认class_weight=None上,小类别梯度完全被淹没。后来先把背景区域在mask里用ignore_index=255排除掉,再给前景类别手动加权,效果才上来。所以如果你的背景占比超过95%,直接在标注阶段就把背景排除出损失,比重写一个loss有效得多。
4.3 训练中断了怎么办:断点续训和权重转换的正确姿势
训练中途因为机房断电、显存炸了、或者自己手抖Ctrl+C终止,这是家常便饭。mmengine有一套比较完整的断点续训机制,你只需要在命令行加--resume参数指向最近的权重文件即可。但要注意,mmengine会自动在work_dirs下生成last_checkpoint文件来记录最新状态,所以更省事的写法是:
python tools/train.py my_config.py --resume work_dirs/my_experiment/iter_12000.pth也可以不指定具体路径,在config里设置:
resume = True这样它会自动去找work_dirs下最新的权重。这里有一个小坑:resume=True会同时恢复optimizer、scheduler和随机数状态,如果你只是想换一个预训练权重从某个迭代重新跑(而不是严格续训),应该用load_from而不是resume。
4.4 如何利用验证指标反推数据问题
验证集的mIoU如果一直很低,不要急着调模型结构,先去看bad case图。建议保存一个可视化脚本,每过几个epoch就把验证集里表现最差的20张图连同预测结果和ground truth拼成一张大图,一眼就能看出模型错在哪里:是边界糊了,还是某个类别整个漏了,还是小目标完全没预测到。
比如你发现模型在“vehicle”这个类别上mIoU是0,但训练集里明明有该类别的像素。打开bad case一看,发现标注里vehicle的像素值标成了2,而config里classes的第2位是building——类别错位。这种问题单纯调模型是永远调不好的,只能回到数据侧修。
反过来,如果所有类别的mIoU都低但loss很低,那很可能是过拟合了验证集分布,或训练与验证的预处理不一致。检查一下val_pipeline里是否有Resize与训练不同、是否有额外的归一化差异。这类问题的排查思路,本质上就是“把数据管道的每一步打开来,直接对比训练和验证样本的输入张量”。
5. 从跑通到精调:三个让效果更好的小技巧
5.1 预训练权重怎么加载:load_from和init_cfg的区别
很多人分不清load_from和init_cfg到底谁生效。简单说,init_cfg是模型构建阶段给backbone、neck、head各组件单独加载权重的机制,load_from是在整个模型初始化之后、训练开始之前,按key匹配加载整个权重文件。在自定义数据集上,我推荐先用init_cfg=dict(type='Pretrained', checkpoint='pretrain/resnet50_v1c.pth')给backbone加载预训练权重,decode_head和auxiliary_head保持随机初始化。如果你直接用load_from加载一个完整的cityscapes模型权重,由于解码器的输出类别数不一致,会有一堆shape mismatch的警告,虽然mmengine会跳过不匹配的key,但效率很低,还容易让人误以为权重没加载上。
5.2 学习率策略和迭代次数的经验值
mmsegmentation官方config里常见schedule_40k、schedule_80k,这里的40k/80k指的是迭代次数。对一个小型数据集(几百张图),40k迭代基本够用。如果你用默认的poly学习率策略,初始学习率0.01对ResNet50 backbone来说偏大,尤其当batch_size只有2或者4时,建议降到0.005,否则前面几百步容易震荡。
我用下来的经验配置是这样的:ResNet50 + PSPNet,batch_size=4,初始lr=0.005,power=0.9,warmup_iters=1000,总迭代24000,配合RandomCrop(512,512)。这个配置在多个自定义数据集上都能稳定收敛,大家可以直接抄作业。如果你的模型更深(如ResNet101)或用了Transformer类的backbone,初始lr要相应调低到0.0001~0.001的量级。
还有一个经常被忽略的参数:optim_wrapper里的clip_grad。训练分割模型时偶尔会有个别batch产生比较大的梯度,导致loss突然跳高。我习惯在optimizer配置里加上梯度裁剪:
optim_wrapper = dict( type='OptimWrapper', optimizer=dict(type='SGD', lr=0.005, momentum=0.9, weight_decay=0.0005), clip_grad=dict(max_norm=35, norm_type=2))这个max_norm=35是mmseg官方几个config里出现过的值,能有效防止loss spike,又不影响正常收敛。
5.3 可视化检查:不只是看看效果,还能定位数据问题
训练过程中定期做可视化预测非常有用。我一般每5000个迭代跑一次可视化,把原图、标注、预测结果并排输出。除了直观验证效果,还能发现一些数据问题:
比如标注和图像没有对齐(有偏移),可视化里会看到预测结果在物体周围有一圈“描边”误差;又比如某些类别在标注里几乎没出现过,预测结果里大概率也不会出现。这时候与其说模型不好,不如说是数据标注的问题。
mmseg提供了一份可视化脚本,也可以自己写一个简化版:
import matplotlib.pyplot as plt from mmseg.apis import init_model, inference_model model = init_model(cfg, ckpt, device='cuda') img = 'test.jpg' result = inference_model(model, img) mask = result.pred_sem_seg.data[0].cpu().numpy().astype(np.uint8) # 调色板映射 palette = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0]] color_mask = palette[mask] # 形状 (H, W, 3) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(plt.imread(img)) axes[1].imshow(mask) axes[2].imshow(color_mask) plt.savefig('vis_result.png', bbox_inches='tight')这样一张图胜过千行日志,能极大节省排查问题的时间。
写在最后的实操体会
这套流程我前前后后跑过不下十次,从第一次吭哧吭哧调格式调了整整两天,到现在基本半小时内能把一个全新数据集跑起来。最大的体会是:mmsegmentation本身不难,难的是数据侧的纪律性。把自己的标注格式、类别ID约定、目录结构固定成模板,反复使用,你会发现自己省下的时间远比研究模型结构的时间多。
如果你后续要做到更复杂的任务,比如多模态分割、半监督分割,或者想把模型导出到TensorRT部署,最稳妥的进阶路径还是先把这一套基础流程吃透。只要数据管道和config体系理解到位,后面任何新模型加进来都只是换一个config文件的事。希望这篇教程能帮你在自定义数据集上少走几个弯路,直接跑通属于你自己的第一个分割模型。