公司动态
边缘与中线实例分割:从数据清洗到YOLOv8训练实战
简介实例分割是计算机视觉中的基础任务其核心在于对图像中每个目标实例进行像素级识别与分离尤其适用于道路场景中边缘、中线等细长结构目标。此类目标在画面中占比小、背景复杂传统检测框难以精确贴合而实例分割通过像素级mask能够精准提取路沿石、车道分道线等物理边界为自动驾驶、无人清扫车和道路病害检测提供关键感知能力。本文从COCO数据集标注体检入手讨论面积、宽高比、mask连通性等清洗要点并介绍针对细长目标的弹性形变、随机遮挡等增强策略。在此基础上详细讲解YOLOv8-seg在边缘与中线实例分割任务中的训练调参、损失权重调整及多尺度训练技巧并结合推理阶段的骨架提取、断点修补与时序平滑等后处理手段为细长目标分割的工程落地提供完整实践参考。 边缘与中线实例分割听起来是个很小的方向但真正碰过道路感知、无人清扫车或者道路病害检测的朋友应该知道这东西有多刚需。路沿石边缘、车道中线这些细长结构目标在画面里占比极小背景又复杂用检测框去做往往效果稀碎。我手头正好在折腾一批2264张的边缘与中线实例分割数据借这个机会把从数据处理、标注检查到YOLOv8训练踩坑的完整过程梳理一遍给同样在搞细长目标分割的朋友一个参考。1. 为什么“边缘与中线”值得单独做一版实例分割数据集先聊点实际的。很多人一听到“边缘”和“中线”第一反应是车道线检测拿语义分割模型跑一下不就行了真不是这么回事。这里说的“边缘”通常指道路路沿石、路肩边界或结构边缘“中线”则指车道分道线、道路中心线。这类目标和经典的Cityscapes车道线语义分割有个本质区别它们是物理结构边界不是简单的像素纹理标记。在真实项目里这类目标有三个明显特征导致它们必须走实例分割路线类别内多实例并存一条路上可能同时出现多条车道中线路沿在遮挡后断开成好几段。语义分割会把所有中线糊成一个连通域实例分割却能把每一条中线、每一段路沿单独拎出来这对后续的车辆横向控制、路沿碰撞预警至关重要。目标细长且尺度变化剧烈近处的中线在画面里可能占几百像素宽度远处就只剩一两个像素。普通目标检测框的宽高比严重失衡anchor匹配困难而实例分割的mask可以精确贴合这种细长结构。边缘和中线经常互相遮挡、紧邻路沿紧挨着车道线用检测框会把两者框进同一个框里实例分割却能通过像素级mask把它们彻底分开。我这次用的这批数据一共2264张图标注了“边缘”和“中线”两个类别格式是COCO实例分割。图片来源涵盖了城市道路、高速匝道、园区内部路、施工现场临时导改路等光照条件有晴天强光、阴天、逆光、夜间路灯总体来说分布比较杂虽然有挑图空间但足够用来验证模型在复杂环境下的泛化能力。说句实在话2264张图对于实例分割任务来说不算多尤其两个类别还都是细长结构。但这也倒逼我在训练策略和数据处理上下更多功夫后面会详细讲我怎么把这批数据的效果榨干。2. 拿到COCO标注后的第一步先做一轮“标注体检”如果你是从公开渠道下载的数据集或者别人交付的数据千万别急着开训。COCO格式看起来规范但实例分割的标注坑比检测标注多得多。我每次拿到新数据集都会跑一遍完整的体检脚本重点查以下几项。2.1 检查标注面积和box宽高比分布实例分割最容易出现的问题是多边形标注面积异常。边缘和中线都是细长目标如果标注多边形在拐角处少了一个点面积会突然小一大截如果多套了一个闭合圈面积又会虚高。我会对每个实例计算mask面积、box宽高比然后画出分布直方图肉眼筛选异常值。检查时重点看宽高比。正常的道路边缘和中线box宽高比普遍在1:5到1:20之间如果大量实例的宽高比接近1:1基本可以确定标注框画错了——要么把整段道路面当成了目标要么漏标了远处的小目标。2.2 验证mask连通性和孔洞细长结构的mask经常出现两种问题一是多边形自相交导致mask出现空洞二是多个断开的线段被合并标注成一个实例。这两种问题在COCO标注工具里肉眼很难发现但训练时会让模型产生严重的认知混乱——同一段路沿一会儿是实心的一会儿是断的。我处理的办法是用OpenCV的findContours重新提取mask轮廓计算轮廓层级关系。如果单个mask内部存在多个“岛状”子轮廓说明标注时把遮挡物中间的区域也填进去了这种标注必须修正。import cv2 import numpy as np def check_mask_contours(mask): 检查单个实例mask的轮廓结构 mask: 0/1二值图 contours, hierarchy cv2.findContours( mask.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE ) # hierarchy: [Next, Previous, First_Child, Parent] num_valid 0 for i, h in enumerate(hierarchy[0]): if h[3] -1: # 外层轮廓 num_valid 1 if h[2] ! -1: print(f发现孔洞轮廓: 外轮廓{i}, 内孔{h[2]}) if num_valid 1: print(f警告: 该mask存在{num_valid}个不连通的外轮廓) return num_valid批处理时我还会统计每个mask的num_valid把大于1的实例单独导出成图片用VIA或Labelme二次检查。2.3 核对类别不平衡和面积占比边缘和中线这两个类别天然不平衡——中线数量通常远多于边缘而且边缘单条长度更长。我这份数据里中线的实例数量大概是边缘的2.3倍如果不做针对性处理模型很容易偏向中线把边缘当成背景噪声。面积占比也要算。细长目标在整个图像里通常只占0.1%~2%的像素面积这个比例在COCO格式里不会直接体现但对loss计算影响很大。尤其使用mask loss时如果整图的mask监督信号太弱模型会倾向于把所有东西都预测成背景。这个时候损失函数里对正样本的加权就显得非常重要后面训练部分我会具体说。3. 数据清洗与增强不能让模型靠“运气”学习体检完之后数据清洗是决定模型上限的关键环节。2264张图在实例分割任务里属于中等偏小规模清洗的目的不是单纯删脏图而是保证每一张图都能给模型提供稳定的学习信号。3.1 筛选低质量标注样本我会生成一张“标注质量热力图”——把每个实例的标注点数量、面积、复杂度都投影到图像上人工抽查边缘案例。经验准则是标注点少于4个的多边形、面积小于50像素的实例、宽高比分布异常偏离的实例都要单独提取出来合成一张大图人工确认。为什么面积小于50像素的实例也值得保留对于细长目标远处的中线可能真的就只有二三十像素长。直接删掉会让模型彻底失去对远距离小目标的学习能力我一般选择保留但在训练时通过min_area参数或loss权重去平衡。3.2 数据增强细长目标特有的策略常规的随机翻转、随机亮度、随机缩放对边缘和中线这类目标效果一般甚至有害。我实验后发现三个特别有效的增强组合随机弹性形变模拟相机俯仰角变化和路面起伏造成的几何畸变。对细长目标效果显著但注意形变幅度要控制否则标注的多边形会和图像内容错位。随机遮挡模拟模拟真实场景中车辆、行人、绿化带对边缘和中线的遮挡。可以用随机矩形块覆盖也可以从其它图片里抠出前景目标贴上去。这个增强对提升模型抗遮挡能力非常管用。亮度对比度扰动针对夜间、逆光和阴影场景。我做法是先用cv2.createCLAHE做局部直方图均衡再叠加随机亮度偏移。另外一个关键点切图策略。原图如果是大尺寸比如5000x3000的无人机图或高清相机图直接送进网络会丢失大量细节。我一般先把图按overlap200切分成1024x1024的patch只保留包含标注目标面积超过阈值比如20%的patch。这能让模型在小分辨率输入下也学到足够的局部细节而且显存友好。import albumentations as A train_transform A.Compose([ A.RandomCrop(1024, 1024, p1.0), A.HorizontalFlip(p0.5), A.ElasticTransform( alpha1, sigma50, alpha_affine50, border_modecv2.BORDER_CONSTANT, p0.3 ), A.RandomBrightnessContrast( brightness_limit0.2, contrast_limit0.2, p0.5 ), A.CLAHE(clip_limit2.0, tile_grid_size(8, 8), p0.3), ], bbox_paramsNone, keypoint_paramsNone)这里特别提醒ElasticTransform对标注的mask也会做同步形变所以使用时要确认标注格式支持。Albumentations里会同步对mask做变换但如果你用其它库一定要验证变换后mask和原图是否对齐否则训练出来的模型会有系统性偏移。4. YOLOv8实例分割训练参数、效率和效果的三方平衡数据准备好了模型选型方面我首先考虑了YOLOv8-seg。原因很简单训练效率高、部署方便、自带丰富的增强策略而且实例分割的mask输出对边缘和中线这种目标足够精细。不过要想让它在这种细长目标数据集上发挥出效果有不少调参细节得注意。4.1 基础配置说明我用的是yolov8m-seg输入分辨率设为1024x1024。选m而不是s是因为细长目标的边界需要更细的mask头来拟合s的backbone在浅层特征的细节保留上略有不足。v8n-seg我用过一次速度倒是极快但在边界轮廓的精细度上明显不如m。数据集文件按YOLO格式组织dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml每个实例的mask转成YOLO分割格式时要保存成归一化的多边形坐标class_id x1 y1 x2 y2 ... xn yn这一步用pycocotools读出COCO的多边形标注再归一化写入txt。注意YOLO格式中多边形顶点坐标是相对于图像宽高的比例不是绝对像素值。data.yaml内容train: dataset/images/train val: dataset/images/val nc: 2 names: [edge, centerline]4.2 训练超参数的核心调整点以下是实际效果最明显的几个参数调整batch细长目标对batch size非常敏感。我用了batch8单卡A100 40Gbatch太小会导致BN统计量波动让mask头很难收敛。epochs小数据集上训练200~300epoch比较合适我实际选在240。过多会过拟合过少细长目标的边界拟合不充分。imgsz从默认的640提到1024对细长目标的召回率提升非常明显但显存占用会翻倍以上。可以根据实际GPU选择。optimizerSGD和AdamW我对比过AdamW收敛更快最终mAP更高。命令行参考yolo segment train \ datadataset/data.yaml \ modelyolov8m-seg.pt \ epochs240 \ imgsz1024 \ batch8 \ lr00.01 \ lrf0.01 \ optimizerAdamW \ patience40 \ projectruns/segment \ nameroad_edge_exp14.3 Loss权重与mask温度YOLOv8-seg的mask loss用的是BCEWithLogitsLoss细长目标的正负像素比例极不平衡。默认权重下模型会倾向于输出全零mask。我参考了实际测试将mask loss对应的loss_weight从默认值提高了一点不同版本的Ultralytics配置位置略有不同可以通过直接修改loss计算部分的权重系数实现。另外YOLO内部对mask预测温度参数mask_ratio也有影响——它决定了下采样mask的粗糙度。边缘和中线这种结构太小的mask_ratio会把细节磨平。我建议把mask的尺寸控制在和imgsz匹配的合理范围内不要过度压缩。这些参数文件里的具体位置不同版本有差异碰到的同学直接改ultralytics/models/yolo/segment/train.py里对应loss的权重或者通过cfg文件传入。5. 训练结果分析mAP很高但问题藏在细节里训练结束后我发现了三个“隐藏的很深”的问题如果不看可视化结果根本发现不了。5.1 整体mAP表现最终结果mAP500.872mAP50-950.531。只看这个数字可能觉得还行但mAP50-95偏低说明预测mask和真实mask的边界重合度还不高。细长目标本身就容易在边界上有几个像素的偏差一算IoU就被拉低。5.2 问题一边缘类别的“断头”现象可视化后我发现预测出的边缘mask经常在某一点突然中断。路面没有遮挡、没有磨损但mask就是断了。想了一下问题主要在数据标注阶段就存在——很多标注人员在画长线段时看到图片边缘就停了没有把标注延伸到图像边界。虽然视觉上“看起来还能用”但模型学到的是“边缘不应该延伸到边界”训练出来后就会主动提前终止预测。处理办法是回到数据清洗阶段把所有延伸到图像边界的标注实例强制截断到图像边界坐标而不是留几个像素的空白。5.3 问题二中线类别在阴影区域的误检另一类错误是阴影误检。模型把树荫和道路指示牌的阴影误判成中线因为阴影的形状、方向和长度都和中线非常接近。这属于严重误检在自动驾驶场景里会直接导致虚警。解决思路有两个方向一是增加带阴影标注的负样本并在数据增强里加入随机阴影模拟二是在后处理阶段利用中线“颜色亮度较高”的先验知识对mask区域的平均亮度做约束低于阈值的预测直接丢弃。def filter_shadow_mask(mask, image): 基于亮度先验过滤阴影误检 mask: 模型预测的mask (H, W) image: 原图 (H, W, 3) masked_pixels image[mask 0] mean_brightness np.mean(masked_pixels) # 中线通常比阴影亮 if mean_brightness 80: return np.zeros_like(mask) return mask5.4 问题三远距离小目标的漏检第三个问题是远距离小目标漏检。距离超过30米的中线在image里只有十几个像素长模型基本检测不到。细长目标的长尾分布问题在这个数据集上表现得特别明显。为了缓解这个问题我参考了一些多尺度训练经验把mosaic1.0、mixup0.2等增强组合打开并尝试了multi-scale训练动态在0.5到1.5倍之间缩放输入尺寸。从结果看多尺度训练对远距离小目标有明显帮助mAP50-95提升了将近两个点。6. 推理部署时的后处理技巧模型只是第一步训练完了不代表万事大吉。边缘与中线场景在推理阶段有两个特殊需求输出必须是连续的、有序的线条供后续控制算法使用而模型输出的是离散的、可能有断点和噪声的mask。所以推理后处理是必不可少的一环。6.1 从mask到有序点集骨架提取与排序我第一步是用cv2.ximgproc.thinning对mask做骨架化提取宽度为1像素的中心线。然后对骨架点做深度优先遍历把无序的像素点整理成有序的折线点集。这里有个细节分叉点需要特殊处理。如果骨架有分叉通常说明模型误检了非目标结构或两条目标线交叉我会按分支长度排序保留最长分支其余作为噪声滤除。6.2 断点修补与平滑对于模型预测的断头边缘我会做一个RANSAC直线/曲线拟合。如果断点前后的骨架方向一致、间距小于阈值就可以直接用拟合曲线衔接。平滑处理上我推荐Savitzky-Golay滤波器而不是简单的高斯平滑。它能在保持线条几何形状的同时去除锯齿对后续曲率计算更友好。from scipy.signal import savgol_filter def smooth_centerline(points, window11, polyorder3): 对有序点列做Savitzky-Golay平滑 pts np.asarray(points, dtypenp.float32) if len(pts) window: return pts smoothed np.zeros_like(pts) smoothed[:, 0] savgol_filter(pts[:, 0], window, polyorder) smoothed[:, 1] savgol_filter(pts[:, 1], window, polyorder) return smoothed6.3 时序稳定性优化视频流场景下单帧预测的抖动非常明显。边缘和中线如果直接送给下游控制模块会出现转向抖动。我加了一个轻量的时序平滑对连续帧的同一实例用EMA指数移动平均对骨架点坐标做平滑同时做Hungarian匹配维持实例ID的稳定。alpha 0.6 smoothed_pnt alpha * current_pnt (1 - alpha) * prev_pntalpha取值不宜过大否则动态响应会变得很迟钝0.5~0.7之间是比较好的折中。7. 数据集的后续扩展与迭代方向最后聊一下数据规模不够的时候还能怎么“找补”。2264张图做完上面所有步骤模型的可用性已经能应付一部分简单场景但离“产品级”还有距离。我有几个备用方案准备下一步尝试。7.1 结合生成数据补足长尾边缘和中线的结构相对规则可以考虑用合成数据补充长尾场景。比如用Blender或Unity的自动驾驶模拟器批量渲染带标注的路沿和车道线图片。合成和真实数据混合训练时注意控制比例合成数据太多会让模型产生“渲染感”在真实照片上掉点。7.2 自监督预训练如果在目标域上的标注数据不足可以在大规模无标注道路视频上用自监督学习做预训练比如用MAE或对比学习的方法。虽然这需要额外的训练成本但对提升细长目标的特征提取能力很有帮助。7.3 模型蒸馏如果算力只够部署轻量模型可以用大模型如YOLOv8x-seg或Mask2Former在全部数据上训练一个teacher模型再用teacher的输出mask作为伪标注去蒸馏小模型。这在细长目标上尤其有效——teacher能捕捉到的弱边缘和小目标细节小模型自己学不到但通过蒸馏可以迁移过来。这批数据从清洗到训练再到后处理整个过程走下来我对“边缘与中线实例分割”这个方向的理解比一开始深了很多。最核心的心得就一句话别急着训模型先在数据上把细长目标的“脾气”摸清楚。标注面积分布、mask连通性、遮挡规律、阴影分布这些细节直接决定了最终效果的上限。希望你拿着这份经验少走一些我走过的弯路。本文还有配套的精品资源点击获取