ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv8实例分割实战:从Labelme标注到模型训练全流程

YOLOv8实例分割实战:从Labelme标注到模型训练全流程 前阵子在搞一套工业零件的表面缺陷检测需要把零件轮廓从背景里精确抠出来只靠检测框根本不够用。比对了一圈方案最后选了 YOLOv8 的实例分割路线数据标注用 Labelme 来完成整个链路跑通后我发现这组合特别适合自定义数据集尤其是那种样本量不大、类别自己定义的场景。今天把整套流程从环境配置到数据标注、格式转换、模型训练和问题排查完整梳理一遍代码也全部贴在下面后面你自己要做分割数据集时可以直接照着来。1. 整体思路与方案选型1.1 为什么选 YOLOv8-seg 而不是纯语义分割模型要搞清楚这个问题先得把分割任务的类型说清楚。大家常说的图像分割分成两种语义分割是把图片上的每个像素都划分到某个类别里比如把道路、天空、行人全部分开实例分割则更“精细”它不仅要给每个像素分类别还要区分出“同一类别的不同对象”比如图片里有三辆车它得分别把三辆车各自的轮廓抠出来。YOLOv8 里的 seg 模型属于实例分割。它的实现思路很有意思不是像传统分割网络那样做逐像素分类而是走了一条“检测掩码生成”的路线检测头先预测每个目标的位置和类别分割分支再针对每个目标生成掩码。具体来说网络会先生成一组原型掩码prototype masks再为每个实例预测一组掩码系数把原型掩码加权组合起来最终得到该实例的轮廓。这种设计的优点是效率高对小样本也比较友好对一张图里多个同类目标也能正确区分。如果你拿到的任务是“把所有缺陷区域标出来但不关心是不是同一个缺陷”用 YOLOv8-seg 也能做只要在标注的时候把每个独立的缺陷分别画成一个多边形就行模型天然会给每个对象一个掩码。这一点非常符合我们平时做自定义数据集的习惯。1.2 为什么用 Labelme 做标注Labelme 这个工具在计算机视觉领域用得挺广尤其是语义分割和实例分割的场景。第一它完全免费开源有 Python 版本也能直接下载 Windows 可执行文件。第二它支持多边形、矩形、圆等标注类型做轮廓标注首选的 Create Polygons 就在工具栏里操作起来没什么学习成本。第三它输出的 JSON 文件结构简单清晰包含 imagePath、imageWidth、imageHeight 和 shapes 数组shapes 里的每条记录都有 label 和 points后面写脚本转换成 YOLOv8 格式非常方便。有些人可能会想直接用 labelImg 行不行labelImg 做的是矩形框标注也就是目标检测任务用的它没法输出精细轮廓分割模型用不上。所以做分割任务Labelme 基本是默认选择生态也成熟网上资料一大把。1.3 整体技术链路拆解这套流程的核心链路可以理解为环境准备 → 图像标注 → JSON 转 YOLOv8 格式 → 数据集划分 → 训练分割模型 → 结果可视化和部署。每一步看起来都不难但每一步都有坑尤其是格式转换和数据集校验很多新手在这里翻车。我自己做项目时习惯先建立一套标准目录结构后面所有步骤都围绕这套结构来。最终训练时数据集大概是这样的形态custom_dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ ├── 0002.jpg │ └── val/ │ ├── 0101.jpg │ └── 0102.jpg └── labels/ ├── train/ │ ├── 0001.txt │ ├── 0002.txt └── val/ ├── 0101.txt └── 0102.txtimages 放原图labels 放同名的 txt 文件txt 里存的是一行一个目标的分割标注信息。这个结构是 Ultralytics YOLOv8 训练时默认读取的格式准备好之后直接写 YAML 配置文件开训就行。2. 环境配置与 Labelme 标注实操2.1 环境准备Ultralytics 一键安装我建议直接创建一个干净的 Python 虚拟环境避免和日常开发环境互相污染。Python 版本选 3.10 或 3.11 都可以PyTorch 用 2.x。conda create -n yoloseg python3.10 -y conda activate yoloseg pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics labelme如果你的电脑没有 NVIDIA 显卡直接装 CPU 版 PyTorch 也能跑只是训练速度会慢不少小数据集几十张图勉强能接受几百张图就有点煎熬了。正常训练分割模型建议有个 NVIDIA 显卡显存 6GB 以上起步比如 GTX 1660Ti、RTX 3060 这种就能跑 yolov8n-seg 和 yolov8s-seg。Labelme 如果不想折腾 Python 环境可以直接去官网的 GitHub Releases 页面下载 Windows 版本双击打开就用功能完全一样。但是在转换标注数据时需要用到 Python 脚本所以 Python 环境还是绕不开的。2.2 Labelme 标注步骤与操作细节打开 Labelme 后界面很简单左边是图片列表中间是图片显示区域。基本流程是这样的点击左侧“打开目录”选中存放原始图片的文件夹。点击工具栏的“Create Polygons”按钮开始在图片上逐点点击圈出目标轮廓。每点一个点就生成一个多边形的顶点轮廓足够精细后按回车完成这个多边形。输入类别名称比如 defect、edge 这种英文名。点击左侧“保存”保存为同名 JSON 文件。标注的时候有两个细节值得特别注意。第一个是类别名称一定要统一后面转换脚本和训练 YAML 里的 names 顺序都依赖这个字段。比如你第一张图标的是 “defect”后面所有同类目标都必须标成 “defect”大小写、拼写都不能有差异否则转换时会被当成两个不同的类别。第二个是多边形的点不要点得过于密集。很多人在标注曲线边缘时恨不得每两个像素点一个点这会让模型训练速度变慢甚至让掩码出现很多细碎的锯齿。实际操作时我一般是在轮廓变化比较大的地方多点几个点平缓区域隔一段距离点一个点就行模型输出的边界足够平滑。标注完成后如果发现某个顶点位置不对可以点击“Edit Polygons”模式拖动顶点微调也可以删除冗余点。还有一个经验先标注三张图立刻跑一遍格式转换和可视化检查确认流程没问题后再批量标注。这块我在文章第 3 部分会详细说你不提前验证的话等标完两百张再发现坐标归一化写错了那个返工量是真劝退。2.3 标注时需要注意的图像预处理Labelme 不会修改你的图片尺寸标注点坐标记录的是原始像素坐标。如果原图尺寸特别大比如工业相机拍的 4000x3000 像素图模型训练时会对图片做 resize但标注坐标始终是基于原图的。后续转换脚本里会把坐标除以原始宽高做归一化所以训练时不会受到影响。不过大图会显著增加标注工作量也会拖慢训练速度。如果原图过大建议先统一压缩到合理范围比如最长边 1280 或 1600 像素再开始标注。这样标注的时候缩放倍率也稳定转换后的坐标与模型输入尺寸的比例相对统一训练效果更容易保证。3. 从 Labelme JSON 到 YOLOv8 训练格式的完整转换3.1 YOLOv8 分割数据集格式要求YOLOv8 的分割标签 txt 文件每一行对应一个目标对象格式是class_id x1 y1 x2 y2 x3 y3 ...其中 class_id 是从 0 开始的整数后面是轮廓多边形的坐标点所有坐标都除以图片宽高做了归一化所以数值范围在 0 到 1 之间。例如一个类别为 0、轮廓有四个点的目标txt 内容长这样0 0.510742 0.398926 0.485352 0.407715 0.462891 0.407715 0.451752 0.369812这里前两个数 0.510742 和 0.398926 是第一个顶点的 x 和 y 坐标后面依次排列。坐标的顺序要保证是沿着轮廓顺时针或逆时针的连接顺序Labelme 里标注时点的先后顺序已经确定了这个连接关系转换脚本直接保留原始顺序即可。3.2 完整转换脚本JSON 转 txt 并划分数据集下面这个脚本可以一次性完成两件事把 Labelme 的 JSON 文件转换成 YOLOv8 分割格式的 txt 文件同时按比例划分出 train 和 val 两个子集并把对应的图片拷贝到目标目录。这是最省事的方式你只要把自己的路径和类别列表改一下就能跑。# labelme2yolo.py import json import os import random import shutil # 配置部分按实际路径修改 labelme_dir labelme_annotations # Labelme 标注文件存放目录JSON原图 output_dir custom_dataset # 输出的 YOLO 格式数据集目录 val_ratio 0.2 # 验证集比例 seed 42 class_names [defect, edge] # 与标注时的类别名称完全一致 random.seed(seed) # 1. 创建输出目录结构 for split in [train, val]: for sub in [images, labels]: os.makedirs(os.path.join(output_dir, split, sub), exist_okTrue) # 2. 收集所有 JSON 文件 jsons [f for f in os.listdir(labelme_dir) if f.endswith(.json)] if not jsons: raise RuntimeError(未找到任何 JSON 文件请检查 labelme_dir 路径) # 3. 按比例划分训练集和验证集 random.shuffle(jsons) val_count int(len(jsons) * val_ratio) val_json set(os.path.basename(f) for f in jsons[:val_count]) # 4. 转换并拷贝 for json_name in jsons: base_name os.path.splitext(json_name)[0] json_path os.path.join(labelme_dir, json_name) with open(json_path, r, encodingutf-8) as fp: data json.load(fp) img_name data[imagePath] h int(data[imageHeight]) w int(data[imageWidth]) split val if json_name in val_json else train # 拷贝原图到对应目录 img_src os.path.join(labelme_dir, img_name) dst_img os.path.join(output_dir, split, images, img_name) if os.path.exists(img_src): shutil.copy(img_src, dst_img) else: print(f警告图片 {img_src} 不存在跳过该样本) continue # 构造 YOLO 分割格式的文本行 yolo_lines [] for shape in data[shapes]: label shape[label] if label not in class_names: continue cls_id class_names.index(label) pts shape[points] norm_pts [] for x, y in pts: norm_pts.append(f{x / w:.6f}) norm_pts.append(f{y / h:.6f}) yolo_lines.append(f{cls_id} .join(norm_pts)) # 写入 txt 文件 txt_path os.path.join( output_dir, split, labels, f{os.path.splitext(img_name)[0]}.txt ) with open(txt_path, w, encodingutf-8) as fp: fp.write(\n.join(yolo_lines)) print(f已处理: {json_name} - {split}) print(f标注转换完成共 {len(jsons)} 张图片)这里要注意一个问题Labelme 保存的 JSON 里imagePath 字段可能是图片文件名也可能是相对路径。最常见的情况就是 JSON 和图片放在同一个目录下imagePath 直接是文件名上面脚本按这个假设写的。如果你用 Labelme 的“打开目录”方式标注保存的 JSON 和原图基本都在同一目录imagePath 通常是纯文件名所以这个脚本能直接覆盖绝大多数使用场景。3.3 转换后的标签可视化检查这一步很多人会跳过但我觉得它是整个流程里最值得花时间的一步。只要把转换出来的 txt 画回原图上看一眼就能立刻发现坐标是否正确、类别 ID 有没有对应错、轮廓有没有丢失。# check_labels.py import cv2 import numpy as np class_names [defect, edge] img cv2.imread(006.jpg) h_orig, w_orig img.shape[:2] with open(006.txt, r, encodingutf-8) as f: for line in f: parts line.strip().split() if not parts: continue cls_id int(parts[0]) coords np.array([float(p) for p in parts[1:]], dtypenp.float32).reshape(-1, 2) coords[:, 0] * w_orig coords[:, 1] * h_orig coords coords.astype(np.int32) cv2.polylines(img, [coords], True, (0, 255, 0), 2) cv2.putText( img, class_names[cls_id], (coords[0][0], coords[0][1]), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, ) cv2.imshow(check, img) cv2.waitKey(0) cv2.destroyAllWindows()跑完之后绿色轮廓就应该精准地贴合标注目标左上角还会标出类别名。如果轮廓偏离目标几像素是正常的因为标注本身就是肉眼勾的但如果轮廓跑到了图片外面或者坐标点明显乱成一团那就是归一化或者数据读取出了问题回去查 JSON 里的 shape 字段和 txt 内容。4. 训练分割模型与调参实战4.1 编写自定义数据集的 YAML 配置文件训练前要写一个 YAML 文件告诉 Ultralytics 数据在哪里、类别有哪些。路径用绝对路径最稳妥防止后边脚本运行目录不一致导致找不到数据。# custom_dataset.yaml path: C:/work/custom_dataset # 数据集根目录绝对路径 train: images/train val: images/val nc: 2 names: [defect, edge]注意 Windows 系统下YAML 里路径分隔符最好用正斜杠 /否则在解析时容易出现转义问题。nc 必须和 names 列表长度一致names 顺序必须和转换脚本里 class_names 的顺序完全一致否则训练出来的模型标签和实际类别会对不上。这里我习惯在训练前用一段小代码再验证一次数据配置from ultralytics import YOLO model YOLO(yolov8n-seg.pt) metrics model.val(datacustom_dataset.yaml, splitval)注意这不是让你真去完整验证模型而是确认 Ultralytics 能正常读到数据集、标签格式匹配如果数据有问题它会直接报错。实测下来这个“训练前验证”小技巧能拦截掉大部分低级错误省下的调错时间很可观。4.2 训练启动命令与关键参数解读一切就绪后把命令切到 YOLOv8 的 segment 任务即可yolo segment train datacustom_dataset.yaml modelyolov8n-seg.pt epochs200 imgsz640 batch8 device0也可以写成 Python 方式from ultralytics import YOLO model YOLO(yolov8n-seg.pt) model.train( datacustom_dataset.yaml, epochs200, imgsz640, batch8, device0, cacheTrue, patience50, )几个关键参数我说下我的理解model 参数一定要选带-seg后缀的预训练权重比如 yolov8n-seg.pt、yolov8s-seg.pt。如果用普通的 yolov8n.pt模型结构里没有分割头训练时要么报结构不匹配要么训练出来的模型无法做掩码预测。imgsz 默认 640如果你的目标很小比如几百乘几百的图里只有一个指甲盖大小的缺陷建议提到 1024 甚至 1280。代价是显存占用成倍增加训练时间变长。batch 如果显存不够用优先把它减半。6GB 显存跑 yolov8n-segbatch 一般设 4-8 比较稳12GB 以上可以放到 16。patience 是早停参数默认 100意思是如果验证集指标连续 100 个 epoch 没提升就自动停止。小数据集我习惯调到 30-50避免在最后阶段反复震荡浪费时间。如果你的显卡显存比较小还可以把 cache 参数关掉。cacheTrue 会把图像预处理结果缓存到内存里训练速度变快但吃内存改成 cacheFalse 或者不写就用磁盘读取的方式速度慢一点但更省资源。4.3 训练过程监控与结果分析训练跑起来后Ultralytics 会在 runs/segment/train 目录下生成训练日志和 results.png。这个图非常直观左侧是 loss 曲线右侧是指标曲线包括 mAP50、mAP50-95 这些关键指标。很多新手会盯着 loss 曲线不放但我建议你重点关注 mAP50 和 mAP50-95 的走向。loss 降到一定值后会平稳但 mAP 可能还在爬。举个例子我跑过一个小样本项目样本只有 80 张图前 50 个 epoch loss 降得很快mAP50 却一直卡在 0.6 附近到第 120 个 epoch 才慢慢爬到 0.82。这说明模型在前期只是在拟合训练集后期才真正找到泛化的特征。如果你发现训练集 loss 降得很低但验证集 mAP 很拉胯基本就是过拟合了。这时候最有效的办法不是调参而是增加训练数据或者做离线数据增强。最后训练完在 runs/segment/train 下会生成 best.pt 和 last.pt用 best.pt 做推理。推理命令很简单yolo segment predict modelruns/segment/train/weights/best.pt sourcetest_images/ saveTrue跑完之后到 runs/segment/predict 看结果图重点检查模型输出的掩码边界和实际目标的重合度。4.4 常见训练参数调优速查场景建议操作显存不足 OOM降低 batch、降低 imgsz、关闭 cache、换更小模型n 或 s小目标检测效果差提高 imgsz 至 1024/1280或对原图做切片类别不平衡少样本类别多做复制增强或收集更多数据训练不收敛、loss 为 NaN降低 lr0默认 0.01 可降到 0.005检查标签坐标是否越界验证集 mAP 低但训练集高过拟合增加数据量、数据增强或 early stop 提前模型只能框出目标但没有掩码误用了非 seg 预训练权重换成 yolov8n-seg.pt 重训5. 常见问题与避坑清单5.1 Labelme 标注阶段的高频坑第一是中文路径问题。项目路径里一旦出现中文在 Windows 上用 OpenCV 或 Ultralytics 很容易莫名其妙报错有时候找不到图片有时候保存失败。最省心的办法就是所有目录统一英文命名。第二是类别名前后不一致。这个我前面强调过但实际项目里最容易栽跟头。标了 50 张图后发现后面几张标成了 “defect ”多了一个空格或者首字母大小写不一致模型训练时就会把同一个目标当成两个类别典型表现是训练日志里某类的实例数量偏少。第三是 Labelme 默认保存的 JSON 文件名会自动加 .json 后缀保持和图片同名。如果你中途手动改过 JSON 文件名转换脚本里按图片名找 txt 的环节就会对不上所以不要随意改 JSON 的名字。5.2 训练阶段的报错与处理方案我在多个环境里跑 YOLOv8 分割训练遇到最多的三类报错报错一训练直接退出提示“No labels found in ...”。这个几乎都是目录结构问题。要么 labels 目录里没有和 images 里图片同名的 txt要么 YAML 里的 train/val 路径写错了。先检查 images 和 labels 下的文件数量是否一致再逐个看 txt 是否有内容如果 txt 是空文件说明转换脚本里 data[shapes] 没有取到有效目标。报错二loss 曲线直接 NaN。训练日志一旦出现 NaN通常和下采样坐标越界有关。如果 YOLO 格式的某个坐标算出来大于 1 或小于 0模型在计算损失时就会崩。排查方法很简单写个循环读取所有 txt 文件检查所有坐标是否在 0 到 1 之间。如果发现有大于 1 的值多半是 JSON 里 imageWidth 或 imageHeight 读错了。报错三显存 OOM。OOM 是显存不够不是代码问题。先把 batch 降到 2再把 imgsz 降到 480如果还不行就换更小的模型。工业界的经验是如果你的数据不到 100 张图用 yolov8n-seg 已经够了没必要直接上 yolov8x-seg训练集太小的时候大模型只会过拟合得更快。5.3 模型效果不理想的优化思路模型跑出来 mAP 不够高先别急着换大模型按顺序做这几件事第一可视化预测结果确认是漏检还是轮廓不准。漏检说明模型没找到目标可能需要提高输入分辨率或者增加训练数据轮廓不准则说明边界特征学习不够可以检查标注质量看看是不是轮廓点勾得太粗糙。第二看验证集里哪些类别效果差。单独统计每个类别的 mAP 会很有帮助Ultralytics 的训练日志里会打印每个类别的指标。如果某个类别特别差大概率是它的样本数量少或者目标形态差异大需要在该类别上多补数据。第三如果是小目标问题可以尝试 Mosaic 增强和 Resize 策略。YOLOv8 默认开了 Mosaic但其实 Mosaic 对小目标有时候反而有抑制作用因为四张图拼接后小目标会被缩得更小。实在不行可以尝试关掉增强model.train( datacustom_dataset.yaml, epochs200, imgsz640, batch8, augmentFalse, # 关闭所有在线增强 )但这只是临时手段更根本的还是提高标注数据质量和数量。5.4 训练完成后的部署小提示训练完拿到 best.pt 后如果要部署到线上服务一般会先导出成 ONNX再转成其他推理引擎格式比如 TensorRT。导出 ONNX 的命令很简洁from ultralytics import YOLO model YOLO(runs/segment/train/weights/best.pt) model.export(formatonnx, opset12, imgsz640, dynamicFalse)导出成功后会生成 best.onnx之后想在 Python 里验证 ONNX 推理可以直接用 onnxruntime 加载。如果要接 TensorRT需要根据你的显卡环境生成 engine 文件这是另一块比较深的内容这篇先不展开。有一个经验是在转 TensorRT 之前先在原模型和 ONNX 模型上分别跑同一批图片对比输出确认精度损失在可控范围内。如果 ONNX 输出和原模型差异很大多半是导出时的 opset 或 dynamic shape 配置有问题先解决这类问题再去搞测速优化才有意义。我印象最深的一次返工就是在标注前没先验证转换格式一口气标了两百张图最后发现坐标归一化写错了只能重新转换数据集。所以现在我的流程特别固定标完三张图立刻跑一遍转换脚本和可视化检查确认轮廓和类别都没问题再批量推进。这样看起来多了几步操作但省下来的是后面训练失败反复排查的大把时间。后边如果你在类别不平衡、小目标分割或者 TensorRT 部署这些方向上遇到具体问题也完全可以再单独展开聊这几个话题的坑都不比训练本身少。
返回列表