这阵子做的一个城市道路标线检测项目收尾了,代号就叫hx3493。整个项目用Python实现,核心是基于YOLOv8目标检测框架做交通道路标线的自动识别与分类。先交代一下背景:那段时间团队接到一个任务,需要把车载相机采集的道路画面里常见的标线——白色虚线、白色实线、黄色虚线、双黄线、斑马线、导流线这些——准确识别出来,并且能支撑后续的违章判断、车道级导航提醒和路面养护分析。相比单纯做车道线拟合,这种基于目标检测的思路更灵活,能直接输出标线的类型和位置,后续接业务逻辑也更简单。这篇文章基本就是把hx3493从数据、训练到部署的全过程复盘一遍,没有太玄乎的东西,但足够实在,希望对正打算用YOLOv8做交通场景识别或者标线检测的朋友有点用。
1. 项目是这样想的:标线检测到底在检测什么
1.1 标线不是“线”,是一类结构化目标
很多人一听“道路标线检测”,第一反应是车道线检测,脑子里全是Hough变换、二次曲线拟合、图像分割那一套。但hx3493从一开始就没打算走那条路。原因很简单:车道线检测追求的是“连续的车道轨迹线”,输出的是曲线方程或者离散点集;而我们和业务方讨论的时候发现,他们真正想要的是“标线的属性”——
- 这条线是虚线还是实线,是白色还是黄色;
- 这个区域是不是斑马线、导流线或停止线;
- 标线在当前画面里的物理位置和形态边界。
这些东西用回归曲线的思路去做又麻烦又慢,因为斑马线是成片的条纹区域,导流线是斜向网状结构,双黄线又是两根平行实线,让算法用一条二次曲线去表达这些形态,思路就不对。所以方案讨论阶段就把方向定下来了:用目标检测,把每一类标线当作一个独立的检测目标,输出矩形框加类别标签。矩形框哪怕没有闭合的像素级边缘,也能把“标线在哪里、是什么类型”这一层信息给得足够充分。
而且从产品角度说,目标检测的输出天然适合对接下游——比如判断一辆车是否压实线变道,只需要检测到“黄色实线”这个框,再结合车辆定位轨迹算一下框内的横向偏移就行;再比如斑马线区域的礼让提醒,直接看“斑马线”类别框是否出现在本车道前方。这样整套系统的扩展性比传统车道线拟合要舒服得多。
1.2 快速阅读这份需求文件里的信息量
hx3493这个代号其实是我们内部项目管理系统里的流水号,从命名规则可以看出项目被归为“交通视觉识别类”的专项。标题里最关键的信息其实是三块:一是Python,这是我们团队的主力工程语言;二是YOLOv8,模型直接用当时已经非常成熟的Ultralytics官方实现;三是交通道路标线检测,这个就是整个任务的核心场景。
做这类项目之前一定要先搞清楚一件事:**标线检测的场景边界到底在哪。**装在车头的相机看到的画面,和路口高位监控看到的画面,以及无人机俯瞰的路面,差别非常大。hx3493这个项目的场景是车载前视摄像头,视角高度大概在1.5米到2米之间,能看到前方大概30到80米的路面,标线形态呈现近大远小的透视关系。这个约束直接影响后面所有决策:锚框尺度、图像分辨率、数据增强策略、标注规范等等。
我还仔细看了一遍之前类似项目的资料,发现很多团队喜欢一开始就铺一大堆类别,恨不得把水马、路沿石、护栏都塞进模型里。但hx3493的需求里写的很清楚,核心就七种标线类别:白色虚线、白色实线、黄色虚线、黄色实线、双黄线、斑马线、导流线。少即是多,类别定得清楚,模型训练难度和误识别率都能控制住。实际测试也证明,起步阶段真不需要那么多类别,先把主干类别练扎实了,后面再逐步扩展。
1.3 为什么选了YOLOv8而不是老牌方案
在选择具体技术方案的时候,团队内部也简单对比过几类路线。第一种是传统图像处理:边缘检测加颜色分割再走形态学,这套东西在固定光照、固定路况下能用,一旦遇到阴影、雨天、夜间反光就崩得很厉害,而且每一类标线都要单独调参,没法规模化。第二种是用语义分割模型,比如U-Net或者DeepLab系列,能输出像素级标线掩膜,精度很高,但是标注成本太大了,标线本人在画面里占了非常小的像素比例,逐像素手工标注的车载视频数据很难攒到足够数量。第三种就是目标检测,当时在YOLOv8和YOLOv5之间选了YOLOv8,主要是看中几个点:
- YOLOv8的训练流程封装得非常成熟,一个Python包就能解决数据处理、训练、验证、导出全过程;
- 模型本身在检测精度和推理速度上平衡很好,特别是小目标检测能力比之前的版本有明显提升;
- 自带的模型缩放系数多,n/s/m/l/x五档,能灵活适配不同算力的部署环境;
- 官方文档、社区案例非常丰富,万一卡住了能很快找到解决方案。
事实也证明选YOLOv8是正确的,后面训练、调参、导出TensorRT都比较顺。如果你的项目也卡在这个选型关口上,我建议不用再多纠结,直接用YOLOv8起步,先把完整流程跑通比什么都重要。
2. 数据是最关键的一环:标线数据的采集与预处理
2.1 样本采集就得“走车”
我在hx3493里犯过的最大一个错误,就是一开始太迷信公开数据集。网上能下到的那些道路分割数据集虽然质量不错,但很多是外国道路场景,标线样式、颜色、磨损程度和我们国内路况差别非常大。国外常见的蓝色道路标识、黄色路沿,和国内的道路标线体系完全是两码事。第一版模型训出来之后,拿到我们自己采集的视频上一测,mAP比验证集掉了将近20%,原因就是数据分布完全对不上。
后来学乖了,直接用一辆装有车载摄像头的试验车,跑了城区主干道、快速路、工业园区、学校周边几个典型路段,一共录了几十个小时的视频。采集的时候要注意不同时段的覆盖:白天和黄昏的光照差异巨大,树影的晃动路段也特别考验模型的稳定性。雨天的数据这次因为时间关系没有系统采集,但后来在测试中发现雨夜场景确实会有明显掉点,这个后续项目可以作为增量数据补充。
采集是第一步,真正花时间的是抽帧和筛选。车载视频一秒30帧,相邻帧之间的场景重叠度太高,直接全部标注完全是浪费时间。我们用的做法是每两秒抽一帧,然后再人工扫一遍抽出关键帧,把转弯、进出隧道、强逆光这些特征场景保留下来。最终大概从几十小时的视频里整理出了接近2万张有效图片,扣掉模糊、被大车遮挡看不清路面的部分,真正进入标注流程的大约1.6万张。
2.2 标线类别,别想当然定义
标线检测最容易翻车的地方是类别定义。你以为的“白色虚线”和甲方理解的“白色虚线”很可能是两回事。以hx3493为例,我们定的是七个类别,每一类都写了非常详细的标注规范,配合样例图发给标注人员。比如白色虚线要求单条线段的长度大于2米,短虚线不标;白色实线要求视野内可见的连续白色实线被整体标为一个框,而不是一段一段拆开;斑马线区域尽量框住整个条纹组的整体范围,而不是只框一条白条。
另外还有一个让标注过程容易混乱的情况:破损标线该怎么标?路面标线磨损严重、中间断了,按完整标线框还是按碎片框?我们最后定了策略:如果视线范围内大体还能看出是一条标线,中间即使有磨损也当作完整标线框出来;如果已经残破到看不出原始形状,就直接放弃不标,让模型自己去学习规律。这个策略在实际训练中也变得更稳,避免了破损样本带来的噪声。
标注工具用的是开源的图像标注工具,标注结果保存成YOLO格式的TXT文件。每张图的标注文件里每行对应一个目标——类别编号后接四个归一化坐标值x_center, y_center, width, height,全部除以图片宽度和高度归一化。这个格式是YOLO家族的标配,后面训练时直接读文件就行,不需要额外转换。
标注过程中我们还做了个“交叉检查”机制——让同一批图由不同标注员各标一遍,然后比对一致性。因为标线之间的边界确实容易模糊,比如白色虚线靠近白实线的路段,两者紧挨着,到底是一个框还是两个框,全靠标注规范约束了。交叉检查帮我们发现了很多误标漏标,对质量提升非常直接。
2.3 增强策略像做菜,想清楚再放料
数据增强这步看似简单,实际上很考验经验。YOLOv8自带的在线增强默认配置已经不错了,但如果不结合项目场景去理解这些参数的设置,很容易把模型训“怪”。
我们的场景是车载前视摄像头,标线在画面中呈现明显的透视形变——远处的标线很小且密集,近处的标线很大且清晰。模型必须同时应对这两种尺度,这其实是标线检测的一个重要特点。我特意把YOLOv8训练时的图像尺寸设为640,并且在增强参数里把mosaic增强打开,让每个batch里的图片先拼接成4张再缩放到640,这样模型间接看到了更丰富的上下文。同时我关闭了随机翻转(fliplr),因为道路交通场景里左右翻转会改变标线的语义位置——左车道和右车道的标线语义差异太大,翻转后模型学到的特征容易混乱。
光照增强很关键但也要控制幅度。YOLOv8默认的HSV增强参数个人默认值偏中性。我针对车载场景做了小幅上调,把hsv_h设为0.02,hsv_s设为0.7,hsv_v设为0.5。这样的效果相当于让模型见过更多“灰蒙蒙阴天”或者“黄昏暖色调”的样本,让它在真实路况中对光照变化更鲁棒。但幅度也别调太大,否则标线的黄白颜色特征会被破坏,乱了颜色模型就分不清黄线和白线了。
训练数据总量标注完成后大概是1.6万张,我按8:1:1分成了训练集、验证集和测试集。划分时要特别注意同一个路段连续帧的图像不能分散在多个集合里,否则会引入严重的数据泄漏,导致指标虚高。按路段分而不是按图片分,这个细节值很多个点。
3. 训练一个能用的标线检测器的完整实操
3.1 环境配置与Ultralytics依赖
hx3493的训练环境用的是单卡训练,显存24GB,训练YOLOv8m绰绰有余。Python环境用3.10版本,依赖最核心的就是ultralytics这个包。安装特别简单,直接pip install ultralytics即可,它会把底层的PyTorch一起拉起来。
有一点要提醒:如果显卡驱动或CUDA版本比较旧,PyTorch版本冲突是常见问题。建议先把PyTorch单独装好,确定torch.cuda.is_available()返回True之后,再装ultralytics。不然一路顺风顺水到训练时候突然报CUDA错误,排查起来挺折磨人的。
我在实际项目里还喜欢把ultralytics升级到当时最新的稳定版,避免官方旧版本里一些已经修复的bug影响复现。不过也要注意,如果公司内网对包管理管控比较严,那就用anaconda做一个独立虚拟环境,避免污染其他项目的依赖环境。
3.2 数据配置文件与标线检测专用设置
YOLOv8训练已经不需要写模型结构文件了,但数据配置还是需要一份YAML。这份YAML告诉框架去哪里读训练集、验证集图片,以及总共有多少个类别、每类名字是什么。我们的data.yaml如下:
path: /data/datasets/hx3493 train: images/train val: images/val test: images/test names: 0: white-dotted-line 1: white-solid-line 2: yellow-dotted-line 3: yellow-solid-line 4: double-yellow-line 5: zebra-crossing 6: guide-line这里面的类别顺序要和标注文件里的类别编号完全一致,一个数字对不上模型就全乱了。我曾经因为临时调整类别顺序但忘了同步更新已有的TXT标签文件,训练了两个小时后才发现loss曲线异常,重来一遍才解决,真的血泪教训。
前置工作完成后,训练数据集文件夹需要按照YOLO格式组织——images里有train、val、test三个子目录,labels目录里也要有对应的train、val、test子目录。图片和标签的命名要完全一致,都是img_000001.jpg和img_000001.txt这样的对应关系。如果发现标签文件缺失,训练时会有一个警告,但模型训练并不会自动跳过错误样本,这一点千万要自己检查清楚。
写一个检查脚本跑一遍目录,确认每张图都有对应的TXT文件、每个标签文件都不为空,能省很多事以后排查的时间。我把这个检查写成了项目里的固定步骤,后面每次新增数据都会先跑一遍。
3.3 训练参数选择与启动命令
启动训练的命令我用的是Ultralytics的Python接口,而不是直接跑命令行工具。因为后续要做推理封装,用Python接口写在同一套代码里更方便维护。
from ultralytics import YOLO model = YOLO("yolov8m.yaml") # 从配置文件初始化模型结构 results = model.train( data="data.yaml", epochs=120, imgsz=640, batch=16, device="cuda:0", optimizer="AdamW", lr0=0.001, lrf=0.01, patience=15, seed=42, mosaic=1.0, fliplr=0.0, hsv_h=0.02, hsv_s=0.7, hsv_v=0.5, )训练轮数我设了120,但实际训练中模型在80轮左右就基本收敛了,后面靠早停机制自动判断,一直没有明显改善就直接停。早在验证集上mAP50的值已经稳定在0.75上下,训练轮数再拉高反而有过拟合风险。
YOLOv8m的模型主页参数很好理解:640的输入分辨率是对推理速度和精度的一个平衡,再往上调到1280对小的远端标线有提升,但推理时间翻了几倍,考虑实际部署机器的算力情况最后选了640。batch size设16是因为24GB显存下这个值可以保持训练稳定,默认的batch太小会导致BN层统计不够稳。优化器试过SGD和AdamW,实测AdamW在这个任务里收敛更快,最终指标也略高一点点。
训练日志记到flyweight文件里和终端都能看,我通常是隔一段时间看一眼loss曲线,重点检查训练loss和验证loss之间的差距是否在拉大。一旦发现验证损失长时间不再下降,就开始早停了。
3.4 评估指标与第一次实测摸底
训练结束以后,Ultralytics会直接在验证集上跑mAP评估。我重点看了两类指标,第一是mAP50,这个指标衡量的是框的IoU超过0.5且分类正确的情况;第二是mAP50-95,这个要严苛得多,从0.5到0.95每隔0.05算一次然后取平均,更能反映出框位置的精确定度。标线检测其实最看重的是mAP50-95,因为下游判断车道偏移时对框的位置精度有要求,框如果一直“戴歪帽子”,后面每一步都会跟着错。
验证集结果达到mAP50为0.86左右、mAP50-95为0.59左右,这个成绩刚看到的时候觉得还不错,但拿到实车录制的测试片段里一测才发现一堆问题。场景稍复杂一点就开始漏检,特别是树荫下的白色虚线、光照角度比较低的黄昏时段、路面写有文字的地方,这些薄弱点单看指标根本发现不了。所以强调一点:**验证集mAP只是门槛,真实场景多段视频的抽帧定性检查才是验收标准。**从第二轮迭代开始,我们每次训练完都固定跑一遍实车视频抽帧测试,把漏检和误检的典型帧截图存档,作为下一轮数据补采和策略调整的直接依据。
4. 推理、后处理与性能评测
4.1 导出模型与推理封装
训练完成后,模型权重文件是一个.pt文件。直接拿这个文件做推理是可以的,但实际项目里不能这么干,原因有两点:一是.pt文件里附加了很多训练信息和优化器状态,体积大,推理速度也慢;二是在布署环境里通常没有Python训练环境,直接丢一个.pt过去很尴尬。
所以推理前先做模型导出。最常用的是导出成ONNX,再做一次TensorRT量化,按部署机器的情况分成FP16和INT8两种。FP16基本无损精度,速度已经是PyTorch的几倍;INT8还有进一步提速,但标线检测中黄线和白线的颜色特征对量化比较敏感,实测INT8会让mAP掉3到5个百分点,不到万不得已不建议部署端用INT8。
这里给一个导出ONNX最小的Python脚本:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") model.export(format="onnx", imgsz=640, half=True, simplify=True)导出的ONNX文件可以直接用onnxruntime加载推理。TensorRT的导出我一般在部署机器上用官方工具链完成,不在训练机上做,避免驱动的版本差异。
推理封装的代码需要处理输入图像的预处理、模型推理、输出解码和后处理。Ultralytics自带的后处理接口已经很好用,但在对延迟要求苛刻的场景里,直接用ONNX原始输出的[N, 84, 8400]维度张量自己解析,反而更能把握性能。N是batch大小,84是4个框坐标加上80个类别得分(在我们这里是7个类别,所以维度写作[N, 11, 8400]更准确,即4 + 7)。8400是YOLOv8在不同尺度特征图上的预测锚点总数。解析时先过滤掉置信度低于阈值的框,再做NMS(非极大值抑制)去除重叠的检测框。
4.2 后处理:过滤、映射和绘制
后处理这块的细节直接影响可视化效果和下游业务逻辑。hx3493里我定了一个阈值体系:
- 置信度阈值(conf_thres)默认设为0.25。这个值太低会产生大量误检框,太高又容易漏检,需要根据实际场景动态调整。
- NMS的IoU阈值(iou_thres)设为0.45,标线框之间重叠度本来就较高,阈值太小会导致一个长标线被拆成好几个框。
对于每一帧画面,后处理输出的数据结构是检测框的四个坐标值、类别编号和置信度。不同类别可以分配不同的显示颜色,比如白色线用白色框,黄色线用黄色框,斑马线用亮绿色框,这样可视化的可读性会高很多。
针对下游应用还做了一个额外的过滤规则:连续多帧检测到同一位置的同一类目标时,只保留一次结果,把置信度在时间维度上做一个加权平滑,这样输出的标线位置更稳定,不会出现一帧有一帧无的抖动。实现上很简单,就是按位置做聚类,类内置信度取平均值。这个模块在后面接入业务方测试时反馈非常好。
4.3 性能评测不只是看FPS
推理速度要区分两个概念:一个是模型本身的推理时延,另一个是包含图像解码、预处理、后处理在内的完整端到端时延。项目验收和做技术方案的时候,端到端时延才是真正可靠的指标。hx3493部署在工控机上,测试环境是TensorRT FP16,输入分辨率640,单帧端到端耗时大约28毫秒,换算下来每秒能处理35帧以上。这个成绩对实时车载场景已经完全够了。
FPS数值好看还不够,还要看耗时稳定性。有时候平均耗时很低,但偶发一次300毫秒的卡顿,应用层面就会感觉明显一卡。我们在压测时专门统计了95分位和99分位耗时——95分位大约35毫秒,99分位42毫秒,分布看起来很稳定,没有明显尖刺。方法就是在代码里对每一帧推理计时,记录全部分位数,而不是只打印一个平均耗时。
规格之外的事:内存占用的峰值也要控制。车载部署环境有时候内存并不宽裕,GPU显存更是宝贵。ONNX Runtime或TensorRT的显存占用远低于PyTorch,实测TensorRT FP16下显存峰值大概在1.2GB,这个数据也要在项目文档里写清楚,方便后端同学做资源预算。
5. 现场实测常踩的坑与排查思路
5.1 斑马线和远端小目标为什么老漏检
第一轮实车测试,最大的问题就是斑马线漏检率特别高。后来分析了一下,原因是斑马线的“正样本”在整幅画面里占比太小了——车辆离斑马线还有几十米时,斑马线在640分辨率的图像里可能只有一两百个像素,模型很难识别它到底是什么。加上斑马线是多个白色条纹组成的条状区域,框的宽高比非常极端,目标检测模型对极端长宽比的敏感度有限。
解决办法有二。第一是在标注时把斑马线的框尽量标准,不要标成一个大框把整个路口都框进去,也不要只标其中一条条纹,最好是按斑马线整体区域标,覆盖所有条纹但不要包含过多路面背景。第二是适当给远端斑马线的样本更多的采样权重,在数据准备阶段多保留一些斑马线离车较远的图片。
还有一个更直接的办法是把输入分辨率提高。实测将推理尺寸从640提高到960之后,远端小目标的召回率有明显提升,代价是耗时从28毫秒涨到55毫秒左右。如果算力允许,对“小目标漏检”这类问题,提高输入分辨率往往比换更大模型更划算,值得先试。
5.2 黄白线误检的根源:颜色特征受光照干扰
另一个反复出现的问题,是黄色虚线和白色虚线在特定光照下互相误检。尤其是傍晚光线偏暖时,白色的标线会被相机拍出淡黄的颜色,模型就分不清了。这也是为什么我在训练时特别关注HSV空间上的色相增强,但单纯靠增强还不够。
后来排查发现,模型其实需要同时利用颜色特征和形态特征才能区分黄白线。如果只看颜色,任何光照变化都能让模型失灵;如果只看形态,虚线、实线之间的区分又容易混淆。所以模型训练数据里必须有足够多的“黄色白色在相近光照条件下同框出现”的样本,让模型学会两者相对比的颜色关系,而不是孤立地判断“这个像素偏黄”还是“这个像素偏白”。
实际处理时我们从两个方向切入:一是采集阶段特意增加了傍晚和阴天时段的数据量;二是在后处理阶段对黄白线类别加了一个基于饱和度的辅助校验——当某个框的置信度介于两个类别之间犹豫不决时,查看框内像素的平均饱和度,明显偏低就更倾向于判为白色线。这个方法很轻量,但实测把这类误检减少了不少。
5.3 压缩量化和工控机上跑不动的优化路线
项目后期要做边缘部署时,尝试过把模型INT8量化再部署到低算力设备上。对比下来,模型尺寸是明显小了,但精度掉得比我预想的更厉害,而且掉点主要集中在黄色实线和导流线这些对细节敏感的目标类别上。所以最终采用了FP16方案,精度与速度的平衡更好一些。
如果后续有硬性要求必须用INT8部署,我的建议是要做量化校准集——收集几百张有代表性的真实路面图,喂给量化工具做校准,而不是直接全程默认参数。校准集要覆盖不同光照、不同路面材料、不同标线磨损程度,不然量化后的模型只会在一两种路况上表现正常,换个场景性能就崩。
真到了纯CPU机器上还要继续优化,可以走一条更激进的路线:蒸馏一个YOLOv8n作为学生模型,让训练好的m模型作为老师,把输出特征蒸馏给学生。这样小模型在CPU上跑得动,同时精度能基本接近大模型。这条路线在hx3493里没有完整实施,但小范围验证过可行,算是一个后续调优的方向。
6. 最后分享一点这个项目里最让我有感触的东西
做完hx3493这个项目,我最大的体会是:目标检测模型的精度并不只取决于模型结构本身,数据质量、类别定义、训练参数、后处理策略,每一个环节都会贡献十个点的差距。YOLOv8给了我们一套很顺手的工具,但具体怎么用、怎么排雷,还是要靠对场景本身的理解。
还有一点想嘱咐做类似项目的人:评估模型一定不要只看验证集指标,一定要把模型拿到真实数据上看几段连续视频的表现。很多问题在静态指标上完全看不出来,但在视频时间轴上会暴露得很明显,比如闪烁、漂移、误跳变。hx3493后面连续视频测试时发现的一个问题——个别类别在车辆颠簸时会间歇性漏框——最终通过后处理阶段的时间平滑解决了。这个细节如果在静态测试阶段就放过,上线后使用者一定会发现各种不对劲。
如果这个项目还能继续扩展,我会优先做三件事:一是补齐雨夜和隧道场景的数据,让模型在各种复杂光照下更稳;二是把检测框和车道级定位信息结合,直接输出每条标线的实际物理距离;三是尝试做标线磨损程度的细粒度分类,这个对交通养护部门会很有价值。标线检测看似是个小任务,但真正深入进去之后你会发现,它和整个智能交通系统之间的连接比想象中要深得多。