YAOTU INSIGHTS

YOLO图像分割实战:语义与实例分割的像素级掩码生成指南

YOLO图像分割实战:语义与实例分割的像素级掩码生成指南
简介基于YOLO目标检测算法实现的图像语义分割与实例分割完整工程内含源码、图片数据集和说明文档面向计算机、电子信息工程、数学等专业大学生的课程设计、期末大作业或毕业设计适合有一定编程基础、能自行调试与二次开发的读者作为参考资料。压缩包共2000个文件以958个C/C头文件.h、563张PNG图片、165个inline实现.inl、150个C头文件.hpp和85个CUDA头文件.cuh为主另有51个.c、10个.cu、4个.py源文件及说明文档包体约63.55MB目录按算法模块与数据分层组织便于定位模型、配置和样本。已有1113人学习下载。通过源码可了解YOLO网络结构、检测与分割分支的衔接方式配合图片数据集可快速验证模型效果说明文档则梳理了编译依赖、运行流程和关键函数能帮助读者从环境搭建到结果可视化走通全流程并为后续扩展分割类别或改进网络提供基础。1. 让YOLO输出像素级掩码这个资源包补的是检测与分割之间的路实际项目里经常遇到这样的需求检测系统已经用YOLO把人、车、裂缝框出来了下一版验收却要求“把目标从图里抠出来”要算面积、算覆盖率、换底色。检测框给的是位置像素归属得另想办法。这个标题的 .rar 把基于YOLO目标检测算法实现图像语义分割和实例分割的三样东西打包在一起源码、图片数据集、说明文档省掉的是从零搭U-Net、重新标数据、调loss那套完整流程。它适合两类人刚入门yolo、想用最短路径拿到掩码的新手以及手里已有检测结果、需要把框升级成像素级ROI的从业者。这篇文章按“选型→数据转换→训练调参→避坑→延展”走一遍照着能复现。2. 三条通路怎么选检测框内分割、带分割头的YOLO、实例后处理2.1 检测框内再分割适合简单背景的快速语义分割先讲一个容易误解的点语义分割和实例分割不是非得靠一个什么高级网络才能做。YOLO把实例的包围盒框出来之后框内再做一次像素级二分类前景/背景同样能得到可用的语义掩码。当目标和背景的灰度差异明显——传送带上的划痕、零件表面的油污、单据上的手写笔迹——这条路连训练都不用OpenCV几十行就能跑。import cv2 import numpy as np def segment_inside_boxes(image_rgb, boxes): boxes: [[x1,y1,x2,y2,conf,cls], ...]来自YOLO检测输出 h, w image_rgb.shape[:2] mask_full np.zeros((h, w), dtypenp.uint8) for x1, y1, x2, y2, conf, cls_id in boxes: crop image_rgb[y1:y2, x1:x2] gray cv2.cvtColor(crop, cv2.COLOR_RGB2GRAY) # 先高斯模糊再Otsu降低传感器噪点对阈值的干扰 blurred cv2.GaussianBlur(gray, (5, 5), 0) _, bin_crop cv2.threshold( blurred, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU ) # 开运算去掉零散椒盐点避免掩码里出现孤立像素 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) bin_crop cv2.morphologyEx(bin_crop, cv2.MORPH_OPEN, kernel, iterations1) mask_full[y1:y2, x1:x2] bin_crop return mask_full代码里的逻辑很直白YOLO给出的 boxes 是N,6的数组包含框坐标、置信度和类别ID对每个框裁出 ROI转灰度后做高斯模糊再用 Otsu 自动算出一个分割阈值。cv2.THRESH_BINARY cv2.THRESH_OTSU这个组合表示阈值参数由算法按灰度直方图自动求解不需要人工指定。高斯核5x5、结构元3x3、开运算迭代1次是按常见目标尺寸给出的经验起点目标越小核越小背景噪点明显时把 iterations 提到2。这套方案的优点是零标注零训练缺点也很明确前景背景灰度分布必须是双峰纹理复杂的背景会让结果直接翻车。2.2 带分割头的YOLO判断源码走的是不是这条主流路线真正谈得上“图像语义分割 实例分割”的工程方案是 YOLOv5-seg 和 YOLOv8-seg 这类带分割头的模型。它们的做法不是逐像素分类而是在检测头旁边多挂一条掩码分支网络先输出若干张 prototype mask原型掩码再为每个实例预测一组系数用系数和 prototype 线性组合出实例掩码。这样做的直接好处是显存可控而且实例天然分开不需要额外做连通域。标题里的源码包大概率走的就是这条路线但你拿到手后要自己确认。怎么判断看模型配置文件。以 Ultralytics 风格的工程为例yolov8-seg.yaml的 head 部分会有Segment模块loss 配置里包含seg_loss训练日志里也会出现mask_loss或seg_loss这一类指标。如果配置文件里只有Detect模块那这份源码的目标检测部分就只是给你做框的分割要靠后处理完成。顺带说一个和 COCO 数据集相关的常识COCO 80 类里 person 索引是0bicycle 是1car 是2这个顺序由你拿到的数据集配置决定不是按视觉习惯排的。很多人改数据集时只改了 names 文件里的名字没有保证索引对应关系一致结果 label 里的0被网络当成 person实际想检测的是裂缝训练不报错可视化却全错位。2.3 实例分离后处理不要忘了分水岭和连通域如果你走的是“YOLO检测 框内分割”这条路线得到的是语义掩码同一类目标的所有像素混在一起分不清哪个实例是哪个。此时要靠检测框先给每个目标一个空间范围然后在框内做连通域或者分水岭把粘连物体切开。def split_instances_by_watershed(mask_bin): mask_bin: 单通道二值掩码255表示前景 # 距离变换每个前景像素到最近背景的距离 dist cv2.distanceTransform(mask_bin, cv2.DIST_L2, 5) _, markers cv2.threshold(dist, 0.3 * dist.max(), 255, cv2.THRESH_BINARY) # 局部极大值区域作为种子每个种子是一个实例 markers cv2.connectedComponents(np.uint8(markers))[1] # 分水岭在种子之间画出分界线 labels cv2.watershed(cv2.cvtColor(mask_bin, cv2.COLOR_GRAY2BGR), markers) return labels关键在于0.3 * dist.max()这个阈值。距离变换值越大的像素越靠近目标中心取最大值的三成作为种子阈值相当于只把“明显是中心”的区域作为分水岭起点粘连严重的物体可以把比例降到0.15让种子更靠近边缘分离更细代价是可能把一个完整目标切碎。cv2.watershed会原地修改 markers返回值中边界像素的值为 -1其余像素的值为各自的实例编号。如果你用的本来就是带分割头的YOLO这一步通常不需要因为输出已经是实例级掩码。3. 把普通图片数据集整成YOLO分割格式转换脚本与两个硬性约定3.1 YOLO分割标注格式类别ID 归一化多边形拿到的图片数据集不可能天生就是 YOLO 分割能直接训练的样子常见的数据集标注格式是 COCO JSON 或 VOC XML而 YOLO 分割要的是独立 label 文件。先看清它长什么样一个图片对应一个同名 txt放在 labels 目录里每一行代表一个实例格式是“类别ID 若干归一化坐标点”。0 0.5019 0.4032 0.5342 0.4210 0.5697 0.4019 0.5982 0.4338 1 0.1102 0.8421 0.1450 0.8320 0.1788 0.8605 0.1901 0.8931第一行的第一个数字0是类别ID和训练配置 data.yaml 里 names 的索引完全对应后面每两个数字是一个点的 xy 坐标全部归一化到[0,1]也就是“像素坐标除以图片宽高”。这里有两个容易忽略的约定第一坐标点顺序可以顺逆时针随意训练时会自行规范化第二少于3个点的多边形会被直接丢弃所以小目标标注在缩放后可能因为点距过近而变成无效样本。3.2 COCO JSON 转 YOLO 分割 txt转换脚本与越界检查多数源码包里都会带一个数据转换脚本但质量参差不齐。我一般会先手工跑一遍转换然后打开几个 label 文件核对再开始训练。下面这个脚本是 COCO polygon 转 YOLO 分割格式的完整逻辑import json, os from pathlib import Path def convert_coco_to_yolo_seg(coco_json, img_dir, label_dir, class_names): os.makedirs(label_dir, exist_okTrue) with open(coco_json, r, encodingutf-8) as f: data json.load(f) images {img[id]: img for img in data[images]} cat_id_map {cat[id]: i for i, cat in enumerate(data[categories])} anns_by_img {} for ann in data[annotations]: if ann.get(iscrowd, 0): continue # 群体标注不适合当单个实例跳过 anns_by_img.setdefault(ann[image_id], []).append(ann) for img_id, img_info in images.items(): w, h img_info[width], img_info[height] lines [] for ann in anns_by_img.get(img_id, []): cat_id cat_id_map[ann[category_id]] seg ann[segmentation][0] # 取第一个多边形 if len(seg) 6: # 至少3个点才能构成面 continue pts [] skip False for i in range(0, len(seg), 2): x_norm, y_norm seg[i] / w, seg[i 1] / h if not (0 x_norm 1 and 0 y_norm 1): skip True # 越界点说明标注本身有错 break pts.append(f{x_norm:.6f} {y_norm:.6f}) if not skip: lines.append(f{cat_id} .join(pts)) label_path Path(label_dir) / (Path(img_info[file_name]).stem .txt) label_path.write_text(\n.join(lines), encodingutf-8) print(fconverted to {label_dir}, classes: {class_names})脚本的核心是那个越界检查很多转换脚本不检查坐标是否落在[0,1]内标注里偶尔会出现一个像素越界点转换后掩码会被拉伸到图片外面训练时损失函数下不去。遇到越界点我选择整段跳过而不是修正因为一个多边形有一个坏点整个多边形都可能标歪宁可丢样本不能丢质量。3.3 data.yamlnames顺序就是生死线数据转换完还要写训练用的 data.yamltrain: ./images/train val: ./images/val nc: 4 names: [hole, scratch, edge, stain]train 和 val 指向图片目录label 文件会自动对应到同名前缀的 txt。nc 是类别数names 的顺序必须和转换脚本里 class_names 完全一致因为 label 文件里只存索引不存类名。换数据集时最稳的做法是把 class_names 当成唯一事实源同时注入转换脚本和 data.yaml不要手工去改 names。跑完转换后抽一个 label 文件确认每个类别索引都和预期对应再开始训练。4. 训练与验证实操YOLO损失函数拆解、关键参数与指标怎么读4.1 segment train 训练命令五个必调参数带分割头的YOLO训练命令和检测几乎一样只是把 task 切到 segmentyolo segment train datadata.yaml \ modelyolov8s-seg.pt \ epochs120 batch16 imgsz640 device0 \ cacheTrue patience20如果显存紧张model 换成 yolov8n-seg.pt参数量小很多掩码边缘会糙一点。imgsz640是默认输入尺寸小目标多就提到1024代价是显存占用翻倍。cacheTrue把图片缓存进内存能显著加快训练前提是物理内存不低于16G内存不够就改成cachedisk。patience20表示连续20轮验证指标不涨就早停防止后段过拟合。这里要单独说下YOLO损失函数。分割版YOLO的loss大致是四路相加box_loss 用 CIoU 约束框回归cls_loss 用 BCE 约束类别dfl_loss 是分布焦点损失负责把框的边界分布学到更准seg_loss 则是掩码分支的损失常见组合是 BCE 加 Dice。训练时终端会打印每一路的数值如果 seg_loss 一直比 box_loss 高一两个数量级先不要慌二者量纲不同但如果 seg_loss 在20轮内没有明显下降大概率是掩码标签本身有问题。4.2 推理与掩码输出拿到mask矩阵、多边形和面积训练完成后的推理代码同样简洁。下面这段会在原图上画框并导出每个实例的掩码矩阵和多边形from ultralytics import YOLO model YOLO(runs/segment/train/weights/best.pt) results model(imgs/demo.jpg, imgsz640, conf0.25, iou0.45) for r in results: if r.masks is None: continue boxes r.boxes.xyxy.cpu().numpy() masks r.masks.data.cpu().numpy() # (N, H, W)每个实例一张二值掩码 cls_ids r.boxes.cls.int().cpu().numpy() polys r.masks.xy # 每个实例的多边形坐标列表 for i, (box, mask, cls_id) in enumerate(zip(boxes, masks, cls_ids)): area int(mask.sum()) print(fcls{int(cls_id)} area_px{area} box{box.astype(int).tolist()})masks.data是模型输出的实例级二值掩码形状是 (N, H, W)和输入图片同分辨率masks.xy是每个实例的轮廓多边形点方便直接画 ROI 或存成自定义 JSON。算面积时用mask.sum()数前景像素即可不需要再做多边形积分。conf0.25调低会冒出大量假阳性框掩码再准也没用iou0.45是 NMS 的阈值目标又小又密时降到0.3能少吞掉一些互相遮挡的实例。4.3 三个指标mAP与mIoU之外掩码要肉眼看训练结束控制台会打印 Segment Metrics 表格里面有检测分支的 mAP50、mAP50-95也有掩码分支的 segment_mAP50-95。我见过很多项目只盯检测 mAP掩码质量肉眼看着已经碎了还直接上线这是要特别提醒的。掩码指标和检测指标不一定同步框回归得好不代表掩码边缘贴合目标。如果手里有带 GT 掩码的独立测试集可以自己算 mIoU把预测掩码和 GT 都缩放到同一分辨率计算交并比后取平均。还有一个更简单有效的检查方法把检测框、掩码和原图三样叠在一起随机抽10张不同光照、遮挡情况的图看一遍。重点看两类问题——小目标掩码是不是被相邻大目标“吃”掉了以及掩码是收在框内还是明显突出框外。突出框外通常说明分割头学到了不合理的形状先验这时要把训练集的归一化坐标范围再核对一遍。5. 实例分割避坑指南5条能让loss正常但结果翻车的坑5.1 标注点多但掩码锯齿严重现象多边形动辄几百个点训练速度下降两成推理出来的掩码边缘毛刺明显。原因密集坐标点不代表高精度反而在放大标注噪声分割头被迫拟合手工标注的抖动。解决转换脚本里对多边形做抽稀用cv2.approxPolyDP按轮廓长度2%的 epsilon 简化点数通常能压缩到原来的三分之一训练更快掩码边缘反而更干净。5.2 类别索引错位标签静默丢失现象训练loss正常下降但验证时某个类别永远 mAP0或者可视化图里类别名和物体对不上。原因转换脚本用的 class_names 顺序和 data.yaml 里的 names 不一致label 里写0网络认为是 person实际数据里0对应的是 hole。解决把 class_names 作为唯一事实源同时注入转换脚本和 data.yaml转换完打开一个 label 文件人眼确认索引和类别对应后再训练。这条值得写进给标注团队的要求里。5.3 小目标掩码质量差接近背景现象小物体的检测框还在掩码只剩一条线甚至直接空白。原因imgsz640 时小目标在特征图上的像素太少分割分支的分辨率不够支撑完整掩码。解决显存允许时把 imgsz 提到1024数据增强里确认 mosaic 和 copy-paste 已开启让网络多见到不同尺度的小目标转换脚本里把小目标的最小面积过滤阈值调低很多源码包的默认阈值会误伤小物体。5.4 batch从32降到8后loss不收敛现象显存不够把 batch 调小其余参数不变重训loss 停在1.2附近不降。原因YOLO 默认学习率是按 batch16 左右设置的batch 减小后梯度噪声变大需要同步降学习率。解决batch8 时把学习率从默认0.01降到0.005并打开 warmup_epochs3先观察前20轮 loss 曲线的下降斜率再继续。血泪经验这个参数改起来很玄学但本质就是学习率必须跟随 batch 缩放。5.5 导出ONNX后掩码分支丢失现象yolo export过程成功但 onnxruntime 加载后输出张量里只有 box 分支的4个数加类别数没有掩码张量。原因部分源码包在封装导出函数时只保留了 Detect head 的输出分割头被裁剪了。解决导出前检查模型配置文件的 export 属性是否包含 Segment 输出用固定 batch1 导出并用 onnxruntime 跑一次随机输入核对输出张量的维度。掩码输出一般是 (1, 32, 160, 160) 这种 prototype mask后面还要再接系数做矩阵乘法不是直接就能用的二值掩码。6. 拿到掩码之后验证、导出与三个延展方向6.1 先做稳定性验证再谈导出部署模型在训练集上指标好看不算数。我习惯从验证集抽20张覆盖不同时段、光照和遮挡情况的图逐张记录掩码覆盖率和面积中位数。覆盖率按 IoU≥0.5 的阈值判断如果正样本比例低于90%先不要部署。面积中位数波动超过±20%说明模型对尺度敏感优先检查训练时是否混入了不同分辨率的原图。验证通过后再导出yolo export modelbest.pt formatonnx opset12 dynamicTruedynamicTrue会把 batch 维度设为动态但掩码输出层通常需要固定输入尺寸推理端要自己处理缩放回原图的逻辑。不熟悉动态维度的直接用默认固定尺寸导出更省心。所谓一键部署脚本往上抽象成统一视觉平台的工程问题那是另一个层级的坑。6.2 三个值得尝试的延展方向第一个是遥感图像语义分割这类图背景纹理强、目标尺度差异大检测框内直接做Otsu那套基本不可用建议用带分割头的YOLO并且训练时关闭 mosaic避免多张小图拼接后破坏大目标的连续性。第二个是D435i深度相机测距yolo把实例掩码当成精确ROI传入深度图取中位数深度比整框取深度准很多尤其是框里包含大量背景空洞时。第三个是学生专注度检测yolo v8这类场景用掩码计算“人体与桌面重叠像素比”比单纯用两个检测框的交并比稳定得多这类联合框架很适合作为统计特征来源。最后说一句教训有一版项目我把卷积输出的掩码直接当成真值喂给下游统计没做连通域检查结果两个粘在一起的零件被算成一个面积返工了三天。从那以后我每次拿到掩码都先跑一遍连通域统计实例数量再确认和检测框数量一致。这个习惯没有捷径但它能省掉大部分线上返工。希望帮到你。本文还有配套的精品资源点击获取