资讯动态

COCO-Stuff语义分割实战:标注、加载与训练避坑

发布时间:2026/9/16 21:29:46 来源:尧图企业网站定制
做语义分割或者全景分割的朋友大概率都绕不开 COCO-Stuff 这个数据集。它算是把 COCO 从只看物体往前推了一大步——原来的 COCO 只告诉你图里有几只猫、几辆车而 COCO-Stuff 把天空、草地、墙面、道路这些没有固定形状的背景区域也给标上了像素级标签。这件事看起来不起眼实际上决定了你的模型能不能真正理解一整张场景而不是在空白背景上抠出几个目标框。我最早接触它是在做一个室外场景解析的项目当时手头只有 COCO 的实例标注训练出来的模型一遇到大片草地和天空就开始瞎猜换成 COCO-Stuff 之后整体 mIoU 直接涨了一截。所以这篇就围绕 COCO-Stuff 怎么用展开把版本选择、目录结构、标注格式、加载代码、训练评估和踩过的坑一次讲清楚。不管你是刚开始碰语义分割的新手还是想从 Cityscapes 换到 COCO-Stuff 的老手应该都能从里面抠出点能直接抄的东西。1. 先搞清楚 COCO-Stuff 到底是个什么数据集1.1 thing 和 stuff 这两类标注的根本区别要理解 COCO-Stuff先得把 thing 和 stuff 这两个词掰开。thing 指的是有明确边界、可以数出个数的物体比如人、车、狗、杯子每个实例都能单独框出来这就是 COCO 原本的实例分割标注。而 stuff 指的是那些没有固定形状、边界模糊、通常连成一片的区域像天空、草地、水、路面、墙壁、地毯你没法说图里有三块草地这种话它们更像是一张图的底层材质和空间背景。COCO-Stuff 的核心贡献就是给 COCO 图像补上了这层 stuff 标注。原始 COCO 有 80 个 thing 类COCO-Stuff 在此基础上又加了 91 个 stuff 类合并成 171 个类别的完整体系。这么一来的好处是模型不光学到这里有一只狗还学到狗站在草地上、后面是天空和树场景理解一下子就完整了。做全景分割、语义分割、图像生成、场景图分析这些任务没有 stuff 标注基本上就是残缺的这也是为什么很多分割模型的主干训练数据集都会选它。我第一次看到 label map 的时候还愣了一下因为整张图密密麻麻全是颜色块几乎没有黑边跟 Cityscapes 那种稀疏标注完全是两种观感。后来想明白了Cityscapes 是行车记录视角只标了在路上能看见的东西其余全 ignoreCOCO-Stuff 是尽量把整张图都覆盖上所以你拿它训练的时候模型看到的监督信号要密得多。1.2 COCO-Stuff 10k 和 164k 的取舍逻辑COCO-Stuff 有两个主要版本选错版本能把人折腾够呛这里必须说清楚。COCO-Stuff 10k是 2016 年最早的版本包含 10,000 张图像标注非常密集平均每张图能标出十几个 stuff 类细节完整、覆盖充分适合做研究、做类别关系分析、做精细的分割实验。COCO-Stuff 164k是 2017 年发布的覆盖了 COCO 2017 全部的 164,062 张图像但代价是每张图的 stuff 标注相对稀疏平均每张图只有一两个 stuff 类因为它是先在 thing 标注基础上补充的。所以选择逻辑很简单如果你追求标注密度和实验的干净程度比如研究类间混淆、做小规模消融选 10k如果你要大规模训练一个能落地的分割模型需要海量图像撑住泛化能力选 164k。我个人的习惯是 164k 做预训练10k 做下游的精细评估两边各取所长。还有一个坑两个版本的图像 id 是重叠的文件名也长得很像如果你同时下载下来又不小心把路径配混了训练集里混进验证图像是常有的事。我建议在磁盘上就用两个完全独立的目录别偷懒共用images/。1.3 171 类体系是怎么拼出来的171 类不是随便凑的它的编号空间其实排到了 182。具体是stuff 类占用 id 1 到 91thing 类占用 id 92 到 182中间有一些 id 实际没被使用。你如果在 label map 里打印np.unique()会看到一堆不连续的数值这不是数据坏了而是它直接沿用了这套 id 编排。这里带来一个非常关键的认知label map 里的像素值就是类别 id不是从 0 开始的连续索引。很多新手直接把这个像素值当成类别序号丢进交叉熵损失结果要么报维度不匹配要么训出来的模型全乱。正确做法是建一张映射表把用到的 id 重新映射到 0 到 170 的连续空间或者干脆把网络输出层设成 182 维、把没用到的类别忽略掉。两种都行但一定要清楚自己在用哪一种后面第 3 节我会给具体代码。另外要注意类别分组。COCO-Stuff 官方给了一个粗略的分组比如室外地面室内家具自然景观等做实验时可以把 171 类再聚合成十几类大幅降低训练难度这在算力紧张的时候很实用。2. 下载、解压与目录结构别在这一步就踩坑2.1 官方资源与文件命名COCO-Stuff 的资源主要挂在它的官方项目页上核心是三类压缩包图像包、标注包、像素级 label map 包。164k 版本的图像沿用 COCO 2017 的train2017约 118k 张和val2017约 5k 张label map 单独打包成stuffthingmaps_trainval2017.zip里面是每张图对应的 PNG。标注 JSON 则在annotations_trainval2017.zip里关键文件是stuff_train2017.json和stuff_val2017.json。下载的时候有个实际感受这批包加起来体积不小尤其是图像包解压时最好留出两三倍的空间别解到一半磁盘满了。我吃过一次亏解压过程中断label map 目录里少了上千张图训练时 DataLoader 找不到文件直接崩排查了半天才发现是解压不完整。所以解压完最好核对一下文件数量train2017和对应的 PNG 数量应该能对上。2.2 推荐落盘的目录树目录结构这东西前期多想十分钟后期省下几小时。我习惯这么组织coco_stuff164k/ ├── images/ │ ├── train2017/ # 原始 jpg │ └── val2017/ ├── annotations/ │ ├── stuff_train2017.json │ ├── stuff_val2017.json │ ├── image_info_train2017.json │ └── image_info_val2017.json └── stuffthingmaps/ ├── train2017/ # 像素级 label map png └── val2017/这样拆开的好处是图像、JSON 标注、像素标注三者职责清晰。JSON 里存的是各类 stuff 区域的轮廓多边形和图像元信息PNG 里存的是直接可用的像素标签。做分割任务时绝大多数人用 PNG 就行JSON 更多是在需要查看区域多边形、或者做检测框转分割的时候才用。2.3 别把 images 和 labelMap 搞混这是个看着很蠢、但真有人犯的错把 label map 的 PNG 当成原图去读或者反过来把 jpg 当成标签。原图是三通道彩色图label map 是单通道调色板索引图。如果你用cv2.imread默认参数读 PNG得到的是三通道很多人还以为读对了其实那三通道是把调色板展开后的伪彩色值已经变了再拿去训练必然出错。正确姿势是读 label map 时明确指定模式比如Image.open(path)之后不要convert(RGB)而是保留原模式看np.array()的结果或者用cv2.imread(path, cv2.IMREAD_UNCHANGED)。判断标准很简单读出来的数组如果 dtype 是 uint8、形状只有两维、取值范围在 0 到 182那才是对的。3. 标注文件格式拆解JSON 和 PNG 双轨制3.1 stuff_train2017.json 里有什么stuff_train2017.json走的是标准 COCO 格式顶层有images、annotations、categories三个主键。images里是每张图的 id、文件名、宽高categories是 171 个类的列表每个类有id、name、supercategory其中supercategory会标明是 thing 还是 stuff 的粗分类annotations里是每个标注区域带上image_id、category_id和分割信息。这里的category_id就是前面说的那套 1 到 182 的 id 空间。你要做类别统计、算每个类有多少张图包含都可以从这里入手。我常用它快速跑一个类别分布看看哪些类是长尾训练时好决定要不要做重采样。需要注意的是JSON 和 PNG 并不是严格一一对应的PNG 是官方从 JSON 多边形栅格化出来的结果直接拿来训练更省事。但如果你要做实例级的 stuff 分析比如统计每张图有几个独立的草地连通域那就得回到 JSON 去解多边形。3.2 labelMap PNG 的像素值真相PNG 标签图是以调色板模式存的每个像素一个字节值就是类别 id0 表示未标注区域。你把它读成数组之后可以这样快速看一眼from PIL import Image import numpy as np label Image.open(stuffthingmaps/train2017/000000000009.png) arr np.array(label) print(arr.dtype, arr.shape) # uint8, (H, W) print(np.unique(arr)[:20]) # 前 20 个出现的类别 id打印出来的 dtype 是 uint8形状是两维unique 值一般从 0 开始稀疏分布。这个数组不能直接当索引进 loss原因前面说过id 不连续。很多人第一次跑训练就卡在这损失函数要求类别数从 0 开始连续编号但你喂进去的是 1 到 182 的原始 id结果就是越界或者类别对不上。还有个细节0 到底算不算一个类。在 COCO-Stuff 里 0 是 unlabeled也就是没标注或者被忽略的区域训练时通常要设成 ignore index而不是当成背景类。这里和有些数据集把 0 当背景的习惯不一样千万别想当然。3.3 类别 id 不连续映射到 0..170 的正确姿势解决办法就是建查找表。先确定哪些 id 真正会用可以从 JSON 的 categories 里读也可以直接扫一遍训练集统计出现的 id。然后import numpy as np def build_lut(valid_ids): # 256 长度的查找表默认全部映射到 255ignore lut np.full(256, 255, dtypenp.int64) for new_id, old_id in enumerate(sorted(valid_ids)): lut[old_id] new_id return lut valid_ids list(range(1, 183)) # 简化处理实际用统计结果 lut build_lut(valid_ids) train_ids lut[arr] # 现在就是 0..170 的连续索引 255 ignore这样送进损失函数就干净了。如果你用的是 mmsegmentation 这类框架它其实已经内置了转换脚本会把原始 id 转成labelTrainIds.png并且约定reduce_zero_labelTrue也就是把 0 视为 ignore。理解了这个机制你再看配置文件里那一堆palette、reduce_zero_label就不会一头雾水了。4. 数据加载与可视化实操附可跑代码4.1 用 PIL NumPy 读一张标注图先把最基础的读图跑通这是所有后续工作的地基。上面那段代码已经给了这里补一个可视化确认标签和原图对得上import matplotlib.pyplot as plt img np.array(Image.open(images/train2017/000000000009.jpg).convert(RGB)) mask lut[np.array(Image.open(stuffthingmaps/train2017/000000000009.png))] plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1); plt.imshow(img); plt.title(image); plt.axis(off) plt.subplot(1, 2, 2); plt.imshow(mask, cmaptab20); plt.title(label); plt.axis(off) plt.show()跑出来如果右图是块状的分区色块跟左图的场景结构大致对得上说明读取流程没问题。这一步看着简单但我强烈建议每次都先跑一遍尤其是换了新的解压包之后能第一时间发现问题。4.2 手写一个 COCO-Stuff Datasettorchvision 自带的CocoDetection是给目标检测用的返回的是框不适合像素级分割。分割场景最好自己写一个 Dataset逻辑很直白import os import numpy as np from PIL import Image from torch.utils.data import Dataset class CocoStuffDataset(Dataset): def __init__(self, img_dir, ann_dir, lut, img_size512): self.img_dir img_dir self.ann_dir ann_dir self.lut lut self.img_size img_size self.ids [f[:-4] for f in os.listdir(ann_dir) if f.endswith(.png)] def __len__(self): return len(self.ids) def __getitem__(self, idx): name self.ids[idx] img Image.open(os.path.join(self.img_dir, name .jpg)).convert(RGB) mask Image.open(os.path.join(self.ann_dir, name .png)) img img.resize((self.img_size, self.img_size), Image.BILINEAR) mask mask.resize((self.img_size, self.img_size), Image.NEAREST) img np.array(img).astype(np.float32) / 255.0 mask self.lut[np.array(mask)] return img.transpose(2, 0, 1), mask这里有个必须记住的点图像的 resize 用双线性标签的 resize 一定要用最近邻。标签图是离散的类别 id你如果用双线性插值会在类别边界处凭空造出中间值比如天空 id 和草地 id 之间插出个不存在的 id训练直接崩。这个坑我第一次写分割 Dataset 时踩得很实找了半天才发现是插值方式的问题。4.3 上色可视化与叠加显示训练过程中想盯一下预测效果离不开可视化。类别 id 直接上色很难看可以用一个固定调色板把每类映射到固定颜色这样不同实验之间的图才能对比。我一般预先定义一张 171 行的 RGB 调色板数组然后def colorize(mask, palette): valid mask len(palette) out np.zeros((*mask.shape, 3), dtypenp.uint8) out[valid] palette[mask[valid]] return out再把原图和预测的半透明叠加能很直观地看出哪块地方分错了。这一步对调参帮助特别大比只看 mIoU 数字有用得多因为你会发现模型往往是在某些特定类别上反复犯错比如把路面和人行道混在一起或者把墙和建筑搞混。5. 训练与评估里的关键细节5.1 评估指标与 ignore 区域分割任务最常用的指标是 mIoU也就是各类 IoU 的平均。实现方式就是在一张 171x171 的混淆矩阵上累加每个像素然后按类计算。这里有个实操要点ignore 区域不参与统计。你可以在累加混淆矩阵时把 label 为 255 的位置直接过滤掉或者用框架自带的ignore_index。别小看这个设置如果忘了 ignoreunlabeled 那块会被算成一个独立的类直接拉低整体 mIoU而且你还找不到原因。我自己写评估循环的时候习惯在累加前先做一次valid (gt ! 255) (gt 0)的筛选虽然麻烦点但心里踏实。另外要注意mIoU 对长尾类非常敏感。有些 stuff 类在全数据集里只占万分之几单个类 IoU 可能一直是 0但它会把平均值拉下来。所以看指标时最好同时看 per-class IoU 和中位数不要只盯平均值。5.2 类不平衡与采样策略COCO-Stuff 的类别分布极度不均。天空、草地、路面、建筑这几个大类能占掉大半像素而像交通锥花瓶这些小类可能几十张图才出现一次。用普通交叉熵训练模型会倾向于把所有像素都预测成大类mIoU 看着还行但小类全军覆没。常见的应对手段有几个。一是类别加权损失按类频率的倒数来加权但要小心权重过头把训练搞不稳一般开方之后再归一化会温和些。二是OHEM 或 focal loss让模型聚焦难样本。三是数据重采样构建采样器时提高包含稀有类的图像被抽中的概率。我一般先上加权损失如果小类还是不行再叠加重采样两招一起用效果比较稳。还有个容易被忽略的点stuff 类的边界本来就很模糊相邻的墙和建筑在很多图上确实难分评估时这类混淆很正常不必过度纠结重点看大类是否稳。5.3 常见框架接入要点用 mmsegmentation 的话COCO-Stuff 164k 有现成配置数据集转换脚本会生成labelTrainIds.png配置里设reduce_zero_labelTrue、num_classes171就能跑。用 detectron2 做全景分割的话它也能注册 COCO-Stuff 格式但要注意 thing 和 stuff 的划分方式跟官方可能略有差异类别 id 要仔细核对。如果自己从零搭训练流程那上面第 3、4 节的映射和 Dataset 就是全部基础。我个人的经验是不要一上来就套框架先用几十张图把数据管线跑通看清楚每个 tensor 的形状和取值范围确认没问题了再接大框架能省掉大量排查时间。6. 常见问题速查与避坑实录6.1 问题排查表下面这张表是我这些年攒下来的高频问题出问题时先照着查一遍能解决八成情况现象可能原因解决思路训练一开始就报类别越界直接把原始 id 当索引用建立 id 到 0..170 的映射表loss 能跑但 mIoU 极低忘了 ignore 0 或 255设置 ignore_index过滤无效像素标签全是伪彩色用默认参数读了调色板 PNG用Image.open原模式或IMREAD_UNCHANGED类别边界出现奇怪颜色标签 resize 用了双线性标签统一用最近邻插值某些类 IoU 恒为 0长尾类被大类淹没加权损失 重采样找不到部分标注文件解压不完整核对图像与 PNG 数量是否一致训练集混入验证图两个版本目录共用10k 和 164k 分盘存放6.2 我踩过的几个坑第一个坑是 id 空间。我最开始以为类别就是从 0 开始连续编号的结果直接拿像素值去算交叉熵网络输出 171 维、标签里却有 182 这种值报错报得莫名其妙。后来老老实实建了查找表才通。所以再强调一遍先搞清楚 id 空间再动数据管线。第二个坑是调色板读图。有段时间我用 OpenCV 读标签默认三通道跑出来的分割结果花花绿绿的怎么看怎么不对。查了半天才发现读进来的是伪彩色展开值根本不是类别 id。这个错非常隐蔽因为代码不报错只是结果慢慢变差。第三个坑是 0 的处理。有的数据集把 0 当背景类我习惯性沿用结果在 COCO-Stuff 上 0 是 unlabeled被我当成背景类训练模型学了半天未标注这个概念白白浪费。后来统一把 0 设成 ignore效果立刻正常。第四个坑是内存。COCO-Stuff 图像分辨率不小label 也是全图如果 batch size 开太大很容易爆显存或者把内存吃满。我的做法是训练时随机裁剪到 512 或 768验证时再整图推理兼顾了效率和指标真实性。裁剪的时候记得图像和标签用同一组随机参数否则对不上。最后分享个小技巧如果你只是想做类别分布分析或者可视化论文配图其实不用下载全部图像光拿 label map 包就能出很多结论体积小、处理快是个很实用的取巧办法。真正要训练了再把图像补齐能省不少来回折腾的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价