ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLO实例分割数据集实战:从标注格式到训练避坑指南

YOLO实例分割数据集实战:从标注格式到训练避坑指南

简介:通用物体实例分割数据集面向目标检测与实例分割模型开发,覆盖人物、汽车、动物等80个常见类别,共972张真实场景图片,按579/245/148划分训练、验证与测试集,并采用YOLO格式多边形标注,便于直接用于YOLO、Mask R-CNN等主流框架。压缩包共1946个文件,包含972个jpg图像、972个txt标注、1个yaml配置及1个docx说明文档,整体约63.44MB,目录结构规整,适合模型训练、算法评估及学术基准测试。这一数据集同样适用于智能监控中的物体分割与跟踪、零售商品识别等场景,类别丰富且标注精细,能有效提升模型在复杂环境下的泛化能力。已有75人学习下载,对于需要构建自动驾驶视觉、机器人感知或进行计算机视觉研究的人员,是一份可直接落地的数据基础。

1. 通用物体实例分割数据集:一个开箱即用的YOLO-seg标注包

通用物体实例分割数据集,这是我在工业视觉项目里实际拆过的一份标注资源。做抓取、质检这类多类目标检测场景的人,最头疼的不是YOLO学不会,而是标注跟不上——框住一个物体容易,把每个物体边界抠到像素级,一帧图就得花十几分钟。这份zip里装的是已经按YOLO实例分割格式排好的图片和txt标注,训练前不用再做坐标换算,解压就能喂给yolov8-seg或yolov11-seg。适合谁?手里有工业相机、想做多类目标检测但缺干净标注数据的工程师,以及刚接触实例分割、想跳过造轮子直接看训练全流程的新手。它能帮你省下最费时间的那一步:标注格式整理。以下内容是我拆包、清洗、训练这一路踩过的真实记录,参数和坑都写具体。

2. 拆包与格式摸底:先看清目录结构,再谈怎么train

2.1 实例分割格式的目录约定:images与labels怎么对齐

拿到通用物体实例分割数据集_20251117_152913.zip,先别急着解压后双击看图片。我一般会先看一眼压缩包内的顶层目录,判断它是裸文件还是标准结构。常见的YOLO-seg数据集结构是这样排的:

dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

这个结构几乎是Ultralytics生态的默认约定:images/train里的每张图,在labels/train里有一个同名txt文件,二者一一对应。txt里每一行代表一个实例,格式是:class_id x1 y1 x2 y2 ... xn yn。注意这里没有框坐标——实例分割的标签只有多边形顶点,而且全部是归一化坐标,即把像素坐标除以图像宽高,值域在0到1之间。训练时模型会根据多边形的外接矩形自动算box,你不需要额外提供bbox。

判断一个数据集是不是真的YOLO-seg格式,比想象中容易踩坑。很多网上下载的资源打着实例分割的名义,实际给的是COCO JSON或者一个mask PNG目录,你要自己转成txt。而这个zip包如果打开后直接就是labels目录加txt文件,说明已经做完了格式转换,是我这种怕麻烦的人最喜欢的形态。解压后建议先把data.yaml打开看一遍,里面至少要有path、train、val、names四个字段,names是一个字典,键从0开始递增。

为什么从0开始这一点很关键?因为txt文件里的class_id是数字,训练时YOLO读的就是数字,names字典的下标决定了这个数字最终显示成什么类别名。如果names里第一个键是1而不是0,哪怕txt里第一行类号是0,寻址也会出问题,轻则类别错乱,重则索引越界直接崩。所以拿到数据集的第一件事,不是看图片长什么样,而是先数一下names的个数和txt里出现的最大类号能不能对上。

2.2 标注完整性校验:坐标范围、多边形顶点数、空文件

解压完成后我习惯写一个短脚本,把整个labels目录扫一遍。这一步能提前暴露大部分训练时会炸的雷,省得训练到一半报错再回头查。脚本逻辑很简单:遍历所有txt,逐行解析,检查三类问题——坐标是否越界、多边形顶点是否足够、文件是否为空。

import os from pathlib import Path label_root = Path("labels/train") errors = [] file_count = 0 for txt in label_root.rglob("*.txt"): file_count += 1 with open(txt, "r", encoding="utf-8") as f: lines = f.readlines() if len(lines) == 0: errors.append(f"{txt}: 空文件,该图无目标") continue for line_no, line in enumerate(lines, 1): parts = line.strip().split() # 最小合法行 = class_id + 3个点(6个坐标值) = 7个元素 if len(parts) < 7: errors.append(f"{txt}: 第{line_no}行元素只有{len(parts)}个,点数不足") continue cls_id = int(parts[0]) coords = list(map(float, parts[1:])) if any(c < -0.01 or c > 1.01 for c in coords): errors.append(f"{txt}: 第{line_no}行坐标越界,出现值 {coords}") if cls_id < 0: errors.append(f"{txt}: 第{line_no}行类别号为负数") print(f"共扫描 {file_count} 个标签文件") print(f"异常 {len(errors)} 条") for e in errors[:30]: print(e)

这段代码的逻辑分三层。第一层处理空文件:如果某张图没有任何目标,对应的txt文件是零字节空文件,这在YOLO格式里是合法的,表示背景图;但如果整个目录全是空文件,那说明标注根本没生成成功,得回去重新导出。第二层检查len(parts) < 7:一个多边形最少需要三个顶点才能构成面,每行元素个数就是1 + 顶点数 * 2,少于7个就不是个闭合区域,训练时会被跳过,但会拖慢数据加载。第三层检查坐标范围:因为使用了浮点归一化,正常值必然在0到1之间,出现负数或大于1的值,多半是转换脚本里忘记除宽高,或者是把COCO的像素坐标直接塞了进来。

边界放了一点余量(-0.01到1.01)是因为有些标注工具会为了贴合物体边缘产生极轻微的越界,例如0.99998这样的值。完全等于1也没有问题,真正需要警惕的是大幅度越界,比如坐标跑到5.7这种,十有八九是单位没换算。我一般会跑完脚本,把报错文件数除以总文件数,如果异常率超过1%,就不会直接用这份数据训练,而是回去做一轮清洗。

3. 从通用数据落行业场景:类别筛选与数据清洗是训练前的必修课

3.1 为什么“通用”不等于“拿来就训”:域差异和类别分布

“通用物体”这四个字是双刃剑。往好里说,类别覆盖面广,瓶瓶罐罐、箱子、零件、车辆都有,适合做多类目标检测的底座数据;往坏里说,它默认你什么都要检测,而工业场景通常只需要其中的三五个类。我见过有人拿通用数据集直接训一个“检测划痕”的模型,结果模型把高光反射当成划痕,因为通用数据里根本没教它区分反光和缺陷——这就是域差异。

通用数据集的类别分布几乎都是长尾:头部类别样本量占了大半,尾部类别可能只有几十张图。如果你需要的恰好是尾部类别,要么补齐标注,要么就得做类别筛选。另外,通用数据集的拍摄视角多为平视或俯视,工业相机的固定机位、固定光源拍出来的图像,和通用数据的背景纹理差距很大。一份通用实例分割数据集,直接训练出来的模型可能在测试集上mAP不错,一到现场就翻车,原因不是模型笨,而是现场背景在训练时压根没见过。

所以我的习惯是:先用脚本扫出这份数据集的实际类别列表和每个类别的实例数量,再根据自己要检测的目标决定保留哪些类、丢弃哪些类。这一步不是可选项,是必修项。把几十个类砍到需要的三五个类,模型的学习负担小很多,收敛速度也快得多。尤其是实例分割,每个类别都要学习多边形轮廓,类别越多,模型参数被平均稀释得越严重,精度反而下降。

3.2 类别重映射与清洗脚本:改txt、删冗余、对齐yaml

类别筛选不能只在data.yaml里改names,因为txt里的class_id是原始编号。比如原始数据集里杯子是0、瓶子是1、背景杂物是4,你只想保留杯子和瓶子,就得把0、1重映射成新模型的0、1,把4整行删掉。写一个重映射脚本,一劳永逸。

from pathlib import Path # 原类号: 新类号, -1 表示丢弃 CLASS_MAP = { 0: 0, # 杯子, 保留并重映射为0 1: 1, # 瓶子, 保留并重映射为1 2: -1, # 背景杂物, 丢弃 3: 2, # 纸箱, 保留并重映射为2 4: -1, # 其他物体, 丢弃 } KEEP = {k for k, v in CLASS_MAP.items() if v >= 0} label_src = Path("labels/train") label_dst = Path("labels_filtered/train") image_src = Path("images/train") image_dst = Path("images_filtered/train") for txt in label_src.rglob("*.txt"): rel = txt.relative_to(label_src) out = label_dst / rel out.parent.mkdir(parents=True, exist_ok=True) valid_lines = [] with open(txt, "r", encoding="utf-8") as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) < 7: continue cls_id = int(parts[0]) if cls_id in KEEP: parts[0] = str(CLASS_MAP[cls_id]) valid_lines.append(" ".join(parts)) # 如果过滤后没有目标,连图片也一并删掉,免得白占加载时间 if len(valid_lines) > 0: out.write_text("\n".join(valid_lines), encoding="utf-8") img = image_src / (txt.stem + ".jpg") if not img.exists(): img = image_src / (txt.stem + ".png") if img.exists(): (image_dst / img.name).write_bytes(img.read_bytes())

这段代码的重点在CLASS_MAP。它是一个字典,键是原始类别号,值是新类别号,-1代表丢弃。遍历每一行txt时,先判断原始类号是否需要保留,需要保留的就把第一列的类别号替换成新号,然后写入新的labels目录。同时,如果一张图的所有目标都被过滤光了,那这张图就属于无目标背景图,对训练没有正向帮助,我一般会直接连图片一起删掉,减少数据加载时的搜索开销。

参数调整上有一个细节:txt.stem + ".jpg"这里假设图片是jpg后缀,实际包里有可能是png或bmp。所以代码里做了两段判断:先找jpg,找不到再找png。如果你们拿到的数据集图片后缀五花八门,可以把判断改成image_src.glob(txt.stem + ".*")取第一个存在的。跑完脚本后,labels_filtered里出现的类别号应该是一个从0开始、无断档的连续序列——如果不连续,训练时data.yaml的names会很难写,有空档还会导致类别数统计错。

4. 跑通YOLO实例分割训练:配置yaml与三个关键超参

4.1 dataset.yaml怎么配:路径、类别名、标签对齐

清洗完数据,接下来就是配置data.yaml。这一步看着简单,翻车率反而最高,因为很多人会把path写成相对路径,结果换了目录就找不到数据。我习惯把所有数据集路径都写成绝对的,省得在不同机器上复现时反复改。下面是一个我实际用过的配置,三类目标,对应上面重映射后的结果。

# 注意:path 建议写绝对路径,避免在不同机器上启动时报路径不对 path: /home/user/datasets/custom_seg_dataset train: images/train val: images/val names: 0: cup 1: bottle 2: carton

path是数据集根目录,train和val填写相对于path的子目录。这里有一个容易忽略的点:YOLO读标签时,是根据图片路径自动推导的,它会把images/train替换成labels/train。所以图片目录名必须叫images,标签目录名必须叫labels,不能改成imgs或annos,否则训练时全部提示标签找不到。如果你想自定义目录名,需要显式字段train_labels,但那是非标准写法,不推荐。

names的键和值必须与txt里的类别号逐一对应。键0对应txt里第一列的0,键1对应1,以此类推。类别名用什么字符串都行,不影响训练,只影响可视化时显示的文字。但如果txt里出现了names里没定义的类别号,训练过程会直接抛异常,日志里会提示“class id out of range”。我在2.2节的校验脚本里已经扫过这个,可以把那一行的检查结果再对一遍。

4.2 训练命令与指标解读:mask mAP和box mAP分开盯

配置完成后,训练命令本身不复杂,复杂的是怎么选模型和超参。直接贴我常用的训练命令:

yolo segment train \ data=dataset.yaml \ model=yolov8m-seg.pt \ epochs=120 \ imgsz=640 \ batch=16 \ device=0 \ patience=30 \ lr0=0.01

model=yolov8m-seg.pt是官方预训练权重,下载后放在当前目录。选m而不是n,是因为实例分割任务对特征的精细度要求比目标检测高,n模型太轻量,mask边缘容易糊。如果显存不够,可以退回yolov8s-seg,但代价是mask的边界细节会差一些。imgsz=640是训练分辨率,如果数据集里物体普遍偏小,可以提到800或960,显存占用会增大,推理时也建议用同一分辨率,否则精度会掉落。

batch=16取决于GPU显存。我实测过,一块24G显存的卡跑yolov8m-seg,imgsz=640时batch最多开到16,再大就OOM。8G显存的卡建议batch=8或者把模型换成s。patience=30是早停参数,连续30个epoch验证集指标不提升就停,这个值不要设太小,实例分割的mAP提升是波浪形的,有时第25个epoch才突然跳一个点。

跑完训练,日志里会输出两个关键指标:box_mAP50和mask_mAP50。这两个要分开盯。box指标反映的是模型找到目标位置的能力,mask指标反映的是轮廓贴合精度。场景是抓取或者缺陷检测,mask指标更重要;场景是目标计数或定位,box指标更重要。别在报告里只写一个mAP,那是自欺欺人。另外验证指标里有mask_mAP50-95,这是多IoU阈值的平均,数值会比mAP50低不少,属正常现象,别被吓到。

这里有一张我整理的超参参考表,针对通用物体实例分割数据集的常用取值区间:

参数常用值说明
epochs100~150实例分割收敛慢,太少了mask学不好
imgsz640~960物体小就调大,注意显存
batch8~16按显存调,OOM就减半
lr00.005~0.01预训练权重继续训,太大易震荡
patience20~30早停阈值,别设太小
optimizerAdamWSGD也可以,AdamW收敛更稳

训练到后半程,我一般会盯val_mask_mAP50(B)这个字段,括号里的B表示best。如果这个值在最后几十个epoch里不再上涨,而且patience触发了早停,就说明模型容量已经吃到瓶颈。这时候不要盲目加epoch,而是回看数据集——某个类别的实例有没有覆盖到不同角度、不同光照,如果没有,补数据比调参有用得多。

5. 避坑指南:标注格式、类别丢失和解压乱码的四个典型翻车现场

5.1 训练启动时报“验证集为空”,训练循环直接卡死

现象:yolo segment train启动后,日志显示train正常加载,但val阶段提示找不到任何图片,或者数据集为空,程序卡在第一个epoch不前进。

原因:我在2.1节说过,YOLO通过images目录推断labels目录。如果清洗数据时把labels/val里的空txt全部删了,但images/val里的对应图片没有同步删除,训练框架在构建验证集时会去遍历images/val里的图片,发现每张图都没有对应标签,直接判定数据集无效。

解决:把标签过滤脚本和图片过滤脚本合成一个,凡是过滤后没有标签的图片,同时从images和labels目录删除。更保险的做法是,训练前跑一遍目录对齐检查:遍历images/val里的每一个文件,确认labels/val里有同名txt,计数不一致就用2.2节的校验脚本查出差异文件。从那次之后,我每次拆新数据集都强制先跑目录对齐。

5.2 mask mAP高但可视化时mask比实际物体小一圈

现象:训练时指标都正常,mask_mAP50到了80以上,但用模型跑推理,把mask叠加到原图上,发现轮廓比真实物体小了一圈,就像给物体穿了紧身衣。

原因:这是标注数据源头的问题。部分数据集的mask多边形点密度不够,尤其在大目标上,一个多边形只有十几个点,拟合圆形或弧形物体时会把边缘拉成直线,训练出来的模型自然学不到外扩的弧度。

解决:检查原始txt里每行的坐标点数分布,统计平均顶点数。如果平均顶点数少于12个,建议对mask做一次稠密化处理,用cv2.approxPolyDP的逆操作插值增加点。常见做法是先把归一化坐标转成像素坐标,然后用贝塞尔曲线拟合多边形边缘,重新采样到每边8~12个点,再写回txt。这能明显改善大目标的mask贴合度。当然还有一个更省事的选择,就是换一个标注更精细的同源数据集版本,前提是你能找到。

5.3 类别id越界,训练中动态报错“class index out of range”

现象:训练跑了一两个epoch后突然中断,控制台报某一行标注的class id大于classes数量。

原因:这是老生常谈的问题——数据集的类别号没有从0开始,或者类别号有跳档。比如原始数据集是COCO格式,类别号从1开始,转换脚本没做-1处理,导致txt里的类号最大是79,而data.yaml里names只写了80个键,索引从0到79还好说,就怕names只定义了三个类别,txt却出现了原始类号12。

解决:不要手动改txt,用3.2节的重映射脚本,把原始类别号统一映射成从0开始的连续序列。如果你确定保留的类别只有三类,就映射成0、1、2,中间不要留空洞。跑完脚本后再用2.2节的校验脚本检查一遍最大类号,确保严格小于names的键数。

5.4 zip解压后文件名乱码,图片和标签对不上

现象:在Windows下用右键解压zip,图片和标签的文件名变成一堆乱码,jpg是乱码,同名txt也是乱码,但两者偏偏能对上;一旦换成Linux解压,jpg和txt的乱码不一致,直接匹配失败。

原因:zip文件在打包时使用了特定编码记录文件名,常见的是GBK。Windows的默认解压工具会自动处理GBK解码,所以显示正常;Linux的unzip默认按UTF-8解码,GBK的字节序列被解释成乱码。图片和txt文件名乱码不一致,是因为某些字符在转换过程中被替换成了不可见字符。

解决:在Linux下用unzip -O gbk指定编码解压,或者用Python的zipfile模块手动解压并重命名。我写过一次血的教训:当时用unzip直接解压了三千多张工业图,文件名全乱,花了两个小时才写脚本批量恢复。从那以后,我只要看到zip是中文文件名,一律先执行unzip -l看一下编码,再决定怎么解压。

5.5 验证集mAP不错,一到现场就疯狂误检

现象:在通用数据集上训练出来的模型,验证集mAP挺好看,但是拿到工业相机现场一跑,把传送带缝隙、螺栓阴影、油渍都识别成了目标物体。

原因:这就是3.1节说的域差异。验证集来自通用数据集的分布,现场的背景纹理、光源角度、镜头畸变在训练数据里完全没有体现。模型学到了通用场景下的“目标特征”,但没学到“背景负样本长什么样”。

解决:两个方向。第一,采集现场背景图加入训练集,作为不带标注的背景类样本,让模型学会区分。第二,把模型输出的原始mask做一次面积过滤和置信度过滤,比如设定面积阈值和conf=0.45以上才输出,误检能压下去不少。别指望只调conf就行,conf调太高会把真目标也过滤掉,还是要从数据侧补。

6. 进阶:把实例分割结果导出为自定义JSON——一个通用导出脚本

训练完模型,最后一个环节是把模型输出转成业务系统能用的格式。很多现场的后端不认YOLO的txt,更常见的是要求一个JSON,包含图像尺寸和每个目标的类别id、多边形坐标。写一个脚本,把YOLO-seg的txt转成这种通用JSON,之后不管对方要的是那份上报格式还是做统计分析,都能一份搞定。

import json import cv2 from pathlib import Path label_dir = Path("labels/val") image_dir = Path("images/val") out_data = [] for txt in sorted(label_dir.glob("*.txt")): # 根据txt文件名找对应图片, 读宽高用于坐标反归一化 img_path = image_dir / (txt.stem + ".jpg") if not img_path.exists(): img_path = image_dir / (txt.stem + ".png") img = cv2.imread(str(img_path)) if img is None: continue h, w = img.shape[:2] items = [] with open(txt, "r", encoding="utf-8") as f: lines = f.readlines() for line in lines: line = line.strip() if len(line) < 7: continue seg = line.split() cls_id = int(seg[0]) pts = list(map(float, seg[1:])) # 归一化坐标 -> 像素坐标 pixels = [] for i in range(0, len(pts), 2): px = round(pts[i] * w, 2) py = round(pts[i + 1] * h, 2) pixels.append([px, py]) items.append({ "category_id": cls_id, "polygon": pixels }) out_data.append({ "image_name": txt.stem, "width": w, "height": h, "annotations": items }) with open("results.json", "w", encoding="utf-8") as f: json.dump(out_data, f, indent=2, ensure_ascii=False) print(f"已导出 {len(out_data)} 张图的标注, 共 {sum(len(d['annotations']) for d in out_data)} 个实例")

脚本的核心是坐标反归一化:txt里存的是0到1的相对值,要把它们还原成像素坐标,必须知道图片的真实宽高,所以用cv2.imread读一遍图片。宽高获取之后,每个点的x乘以宽度,y乘以高度,保留两位小数,输出成[x, y]结构。这样后端拿到JSON,不需要再依赖YOLO环境,直接画轮廓、算面积、统计数量都行。

这个脚本还有一个用途,就是质量核验。我会把导出的JSON再画回图片上,和目标检测结果做重叠对比,确认没有错位。画的过程中如果发现某个类别的多边形面积普遍小了一半,那就是训练时mask的缩放出了问题,回查预处理参数。

拆了这么多数据集,我养成了一个习惯:任何zip包到手,先花十分钟跑校验脚本和解压编码检查,再谈训练。这十分钟省下来的时间,远比后面翻车再去排查多得多。通用物体实例分割数据集本身是个好起点,但真正让模型在现场可靠的是你对数据的每一次核对。希望这份拆包记录帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进