1. 项目概述与技术选型
1.1 为什么选YOLOv8做道路标线检测
交通道路标线检测这个需求,放到几年前大家会条件反射想去用传统图像处理那套思路:灰度化、边缘检测、霍夫变换提取直线,再按斜率归并车道线。但实际接触过真实路面场景的人都知道,那套方案对理想化的高速公路还行,碰到城市道路的停止线、菱形导流线、导向箭头、网状禁停区,基本就崩了。因为这些标线不是单纯的"直线"两种形态,箭头有尖锐夹角,网状线是密集交叉纹理,停止线是实线,还有斑马线这种高频重复图案。传统算子扛不住这种多样性。
我选择了YOLOv8作为这个项目代号hx3493的检测底座,核心原因有三点:
第一是精度与速度的平衡。YOLOv8官方在COCO上的mAP表现不错,而它的推理速度在同类一阶段检测器里属于第一梯队。道路标线检测最终是要跑在路侧设备或者车载嵌入式盒子上,NVIDIA Jetson这类场景里,算力有限但又要实时性,YOLOv8的模型尺寸分级(n/s/m/l/x)给了很好的伸缩空间。我自己先在服务器上用yolov8s做实验,验证可行性后再量化蒸馏到小模型,这条路走得很顺。
第二是YOLOv8的anchor-free设计。老版YOLOv5是anchor-based,你得根据数据集的标线长宽比预先聚类出合适的anchor尺寸。但交通标线的形态跨度太夸张:一条车道线可能宽0.15米、长3米,映射到图像上是细长条;一个导向箭头又接近正方形,下方还有矩形的"尾巴"。这种极端的长宽比差异让固定anchor变得很难受。YOLOv8直接去掉了手动聚类anchor的环节,通过Decoupled Head和TaskAlignedAssigner完成正样本匹配,省掉了一大块调参工作,也更贴合道路标线这种形态分布极不均匀的目标。
第三是训练生态。YOLOv8的Ultralytics库封装得太完整了,数据标注格式就是最简单的txt格式,训练命令一行搞定,而且预训练权重、数据增强策略、模型EMA等细节全部内置。我不是说封装好就一定好,但作为项目开发来说,能快速迭代数据、验证模型,比什么都重要。因为交通标线数据集才是最花时间的部分,不该在训练框架上浪费时间。
这个项目解决的具体问题是:对道路监控摄像头或行车记录仪采集的前视图像,自动检测并分类路面标线,比如白色实线、白色虚线、黄色实线、导向箭头、斑马线、停止线等。输出结果可以用于车道级导航、车辆偏离预警、交通设施健康度巡检等下游任务。适合做智慧交通、辅助驾驶、道路资产管理的开发者参考,也适合想系统入门YOLOv8落地实战的人。
1.2 项目目录结构与技术栈概览
拿到一个机器视觉项目,我习惯先把整个技术栈想清楚,再动手写代码。hx3493项目的完整结构如下:
hx3493-traffic-marking-detection/ ├── dataset/ │ ├── images/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ ├── labels/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ ├── data.yaml │ └── classes.txt ├── weights/ │ ├── yolov8n.pt │ ├── yolov8s.pt │ └── best.pt ├── runs/ │ ├── train/ │ └── detect/ ├── scripts/ │ ├── split_dataset.py │ ├── train.py │ ├── detect.py │ ├── preprocess.py │ └── evaluate.py ├── utils/ │ ├── label_convert.py │ ├── augment.py │ └── metrics.py └── requirements.txt技术栈上,Python版本要求3.9以上,依赖库主要是ultralytics、opencv-python、shapely、numpy、tqdm。其中shapely这个库值得单独拎出来说,标线检测的后处理里经常会用到计算IoU、线段的几何关系,shapely比手写几何计算靠谱得多。
数据集这块,我没有用公开数据集直接躺平,而是混了开源数据加自采数据。公开部分主要找包含交通标线标注的自动驾驶数据集,自采部分用行车记录仪在城市快速路、十字路口、学校门口和高速匝道采集的视频抽帧。混合训练的好处是公开数据能提供丰富的场景多样性,自采数据能保证贴合实际部署路况。后面会在数据章节详细说。
2. 数据准备与标注规范
2.1 交通标线的类别定义与标注方法
道路标线的检测,首先要想清楚一个事情:标线检测的目标是什么?是一整条连续车道线,还是一小段标注框内的标线片段?这个决策直接影响标注规范和模型语义。
我的方案是:标注框内是标线局部片段,而不是完整语义线。理由很直接:车道线可能是几百米长的连续实线,如果用检测框去框一整条线,框的长宽比会达到几十比一,anchor-free也不好处理这种极端目标。更重要的是,一辆车行驶过程中,近处的标线清晰完整,远处的标线逐渐模糊断裂,如果检测目标是"一整条线",远处永远检测不到,模型会学出很强的距离偏见。
所以我把每条标线切成多个检测目标,每个目标是一段1.5米到2米长度的片段。标注框包裹每个片段。这样既照顾了远处标线在图像上变短的事实,又保持了目标尺度的相对一致性。
类别定义上,参考GA/T 850和GB 5768系列对道路交通标线的分类,结合实际图像可达性,我给hx3493定义了10个检测类别:
| 类别ID | 类别名称 | 说明 |
|---|---|---|
| 0 | white_solid | 白色实线 |
| 1 | white_dashed | 白色虚线 |
| 2 | yellow_solid | 黄色实线 |
| 3 | yellow_dashed | 黄色虚线 |
| 4 | white_stop | 停止线 |
| 5 | arrow_straight | 直行箭头 |
| 6 | arrow_left | 左转箭头 |
| 7 | arrow_right | 右转箭头 |
| 8 | arrow_straight_left | 直行加左转箭头 |
| 9 | zebra | 斑马线 |
类别不是越多越好,如果数据量有限,类别太细会稀释每个类的样本量,带来严重的数据不平衡。比如掉头箭头本来在城市里就少,你非要单独拉一个类,那这个类的mAP大概率是崩的。我的策略是优先合并视觉形态相似的类别,等数据积累多了再细化。
标注工具我用的LabelImg和X-AnyLabeling。前者是老牌工具,YOLO格式直接导出;后者是半自动标注工具,可以用SAM作为预标注后端。标线这种重复性很强的目标,半自动标注效率会高很多。我实际测试过,用SAM辅助标注,标注速度能提升60%以上,尤其是斑马线这种重复纹理,SAM的掩膜准确率很高。
2.2 数据增强策略与针对性处理
模型训练时的数据增强与预处理,直接决定标线检测效果的上限。YOLOv8默认开了Mosaic、HSV扰动、随机翻转、缩放等增强,但针对道路标线场景,我额外做了几件事。
第一是截断式Mosaic。默认的Mosaic是把四张图拼成一张,但道路标线是细长目标,拼接过程中如果标线刚好落在拼接缝附近,就可能被截断成两截,导致模型学会把半个箭头判成完整箭头。我在hx3493里调整了Mosaic的拼接逻辑,将画面从4张改成2张拼接,并且把拼接区域从中心线移动改为上下分割。这样虽然降低了增强的多样性,但换来了标线完整度的稳定。
第二是光照扰动自定义参数。交通标线的核心颜色特征是白色和黄色,而HSV空间里对H通道的扰动会直接改变标线的色调。白色标线在色相上接近无色,轻微扰动问题不大,但黄色标线的H值在30~40附近,如果增强参数太激进,黄色可能偏成橙色甚至红色。所以我把Ultralytics默认的hsv_h从0.015调低到0.01,hsv_s和hsv_v适当增强,因为饱和度与明度的变化更接近真实光照变化。
第三是透视拉伸增强。道路标线在图像中的形态强烈受透视影响,同一段标线在图像近处是又宽又大的块,在远处是又窄又小的点。YOLOv8内置的透视增强偏弱,我自己在预处理脚本里加了随机透视变换。具体做法是对图像做四点随机扰动,模拟摄像头在不同角度安装时的画面变化。这么做以后,模型对摄像头安装角度的鲁棒性提升明显。
还有一个小细节很多人会忽略:图像分辨率策略。标线是细小目标,一个小箭头可能只占图像的0.5%面积。YOLOv8默认训练尺寸是640x640,对于标线检测来说偏小。我在训练时把imgsz调到1280,但因为显存限制,使用batch size较小的参数组合配合梯度累积。推理阶段同样用1280分辨率,实测小目标检测的recall能提升至少8个百分点。代价是推理时间变长,实际部署时可以用模型输入尺寸缩放加TTA来平衡。
2.3 数据集的划分与困难样本挖掘
数据划分不能简单随机打乱。因为同一段道路的视频抽帧,相邻帧之间的画面高度相似,如果随机划分,训练集和验证集会出现大量"同源样本",验证指标虚高,部署到新道路时性能断崖下跌。hx3493的数据划分是按视频来源划分的:同一段视频的所有帧归入同一个集合。这样验证集才是真正意义上的"未见场景"。
整个数据集规模我控制在一万两千张左右:训练集约9500张,验证集约1500张,测试集约1000张。比例是8:1.3:0.7。
困难样本挖掘这块,我专门做了一轮hard negative mining。模型训练完第一个版本后,用它在未标注的夜间、雨天、逆光视频上跑推理,把置信度在0.2到0.5之间且出现误检的帧挑出来,人工标注后加入训练集。这一批样本对模型的鲁棒性贡献很大。尤其是雨夜路面反光,标线和灯光倒影混在一起,如果没有这部分样本,模型很容易把光影误检成白色实线。
3. 模型训练与优化实践
3.1 训练配置与超参数选择
YOLOv8训练命令本身很简单,但参数值的选取是有讲究的。我最终的训练配置如下:
yolo train \ model=yolov8s.pt \ data=dataset/data.yaml \ epochs=300 \ imgsz=1280 \ batch=4 \ lr0=0.005 \ lrf=0.01 \ warmup_epochs=3 \ optimizer=AdamW \ weight_decay=0.0005 \ close_mosaic=30 \ cos_lr=True \ patience=40 \ seed=42这里解释几个关键参数的选择逻辑:
- optimizer选的AdamW而非SGD。标线检测的数据集相对小,AdamW收敛快且对学习率不那么敏感,适合快速迭代实验。SGD在超大batch下表现更稳,但batch=4这种情况下AdamW是更省心的选择。
- close_mosaic=30表示最后30个epoch关闭Mosaic增强。这是Ultralytics官方推荐的技巧。Mosaic增强出来的样本跟真实场景分布有偏差,最后阶段切回真实图像分布,让模型微调收敛,可以有效降低验证集上的mAP波动。
- lr0=0.005比默认的0.01低一倍。因为我用的是s模型且batch只有4,梯度估计噪声较大,偏低的学习率更稳。
- imgsz=1280配合batch=4,这意味着显存占用很高。我用的是24GB显存的GPU,如果显存只有16GB,建议imgsz=960,或者batch=2加梯度累积。
data.yaml内容如下:
train: dataset/images/train val: dataset/images/val test: dataset/images/test nc: 10 names: ['white_solid', 'white_dashed', 'yellow_solid', 'yellow_dashed', 'white_stop', 'arrow_straight', 'arrow_left', 'arrow_right', 'arrow_straight_left', 'zebra']3.2 训练过程监控与早期问题定位
训练过程不是把这个命令丢进去就完事,需要盯几个关键指标。
第一个是loss曲线收敛形态。YOLOv8包含box_loss、cls_loss、dfl_loss三类。正常情况box_loss应该稳定下降并最终区间收敛在1.2~1.8之间。cls_loss对应分类任务的难度,如果你的类别之间视觉差异大,比如白实线和黄实线,cls_loss会比较低;如果白虚线和白色实线搞混,cls_loss就会卡住不降。我在第一轮训练时发现cls_loss在训练到第80个epoch后不再下降,通过TensorBoard看预测结果,发现主要错误是把白色虚线检成白色实线,这是因为远距离的虚线在图像上断裂间距不明显,视觉上确实接近实线。这个问题光靠训练解决不了,我的处理是调整标注策略:对虚线标注时,只标清晰的线段主体,不把两个相邻虚线段的中间间隙边缘纳入框内。同时给类别定义加了约束:近距离(图像面积大)虚线必须标为虚线,远距离模糊的片段尝试标为实线。这样一来,模型更倾向于根据"近处细节"判断虚/实,而不是远处模糊猜测。
第二个是验证集mAP50和mAP50-95的同步性。如果mAP50高但mAP50-95低,说明模型定位粗略,框的位置和大小不够精确。这在标线检测里很常见,因为标线边界跟路面背景的对比度在某些光照下很低,框的边缘收敛不精确。我的应对措施是在损失权重里增加box_loss的影响,具体做法是修改ultralytics配置里各loss的gain系数。默认情况下box_loss gain约7.5,我提高到8.5。
第三个要盯的是每个类别的AP分布。YOLOv8训练结束后会生成confusion_matrix.png和results.csv。我在results.csv里统计每个类的AP,发现arrow_left和arrow_right的AP比arrow_straight低不少。原因是左转箭头和右转箭头的形态差异只在箭头尾巴的偏转方向,小目标下难以区分。针对这个,我做了两个改进:一是给箭头类单独做在线旋转增强,因为部分摄像头安装角度不同,箭头方向会有轻微旋转偏移;二是调整loss的class权重,让难分类别在损失函数中占更高权重。我在Ultralytics的loss计算中,通过自定义class_weight参数给箭头类别加了1.5倍权重,效果有改善但不算巨大,这也说明标注质量和样本数量的基础性。
3.3 模型剪枝与性能调优
yolov8s在1280分辨率下推理,在Jetson Orin NX上大概有15到25毫秒的性能表现,但实际部署设备可能是更弱的平台,所以我又做了剪枝和量化两个方向的尝试。
剪枝用的torch-pruning库。YOLOv8的C2f模块里面包含Bottleneck结构,不同stage的通道数逐渐增大。常规剪枝策略是按通道重要度裁剪,但YOLOv8有detect head,剪枝时需保留head部分不剪或者小比例剪,避免输出维度错乱。我剪掉了backbone最后两个stage的20%通道,剪完后需要小学习率微调。微调80个epoch后,mAP50只掉了0.6个百分点,但推理速度提升了接近18%。这个性价比非常高。
量化的坑我踩过一次。用PyTorch的torch.compile或者onnxruntime直接转FP16,输出概率分布很容易出现异常,因为YOLOv8的Detect头里有sigmoid和分布焦点损失相关的计算,浮点精度敏感。我最后是先用TensorRT的PTQ(Post-Training Quantization)校准,选了一千张训练集图片做校准数据,校准算法用entropy而非minmax。INT8量化后模型体积从80MB降到23MB,mAP50下降大概2个百分点,但在嵌入式设备上推理帧率直接翻倍。标线检测对精度要求的敏感度没有行人检测那么苛刻,2个点的mAP损失可以接受。
4. 推理部署与后处理
4.1 推理脚本与参数细节
推理阶段,hx3493的脚本设计包含几个环节:图像输入、预处理、模型推理、后处理、结果绘制与可视化。核心代码如下:
from ultralytics import YOLO # 加载训练好的权重 model = YOLO("weights/best.pt") # 推理参数 results = model.predict( source="test_video.mp4", imgsz=1280, conf=0.35, iou=0.45, max_det=300, classes=[0,1,2,3,4,5,6,7,8,9], stream=True, device=0, verbose=True )这里根据实际调试经验,说明几个最重要的参数选择:
- conf=0.35。初始我按默认的0.25来跑,发现误检比较多,尤其是复杂路面上水渍、阴影、旧标线残留都被框出来了。提到0.35后,误检明显减少。要注意的问题是:conf阈值调高会降低recall,尤其远处小目标,所以需要根据实际部署路况在这个值附近微调。我建议先做一段路测视频的置信度分布统计再定值。
- iou=0.45。标线检测的含框密度高,箭头附近的停止线、斑马线的框会大量重叠,NMS的IoU阈值太严会把相邻标线的框直接抑制掉。
- max_det=300。一幅图里标线目标数量可能极多,尤其是斑马线,一段斑马线会被切成十多个目标框。这个值如果设成默认的300反而可能不够,但设太高会影响NMS效率。
还有一个很容易被忽略的点:推理时的预处理均值方差。Ultralytics的predict接口内置了letterbox归一化逻辑,模型权重训练时会记录预处理参数。如果你用了ONNX或者TensorRT手动推理,一定要用同样的预处理操作:图片按长边等比缩放到1280,短边pad到32的整数倍,像素值除以255。有次我从PTQ模型转TensorRT,因为预处理时忘了pad,推理结果直接全乱。
4.2 结果后处理与车道级矫正
YOLOv8输出的原始检测框是矩形的,但标线本身是长条形的,直接在图像上画矩形框视觉上还行,但用于后续车道级应用就不够了。hx3493的后处理做了三件事:
第一是框的透视矫正与线段提取。矩形框只能表达标线的包围盒,无法表达标线的方向。我在后处理中用每个框内的白色/黄色像素分布做PCA方向分析,从框内分割出标线的主方向,把矩形框转成带角度的线段表述。算方向时,利用标线像素与路面背景的灰度差异做二值化,然后计算协方差矩阵的特征向量,特征值大的方向就是标线走向。
import numpy as np def get_line_orientation(mask): ys, xs = np.where(mask > 0) if len(xs) < 10: return None coords = np.stack([xs, ys], axis=1) mean = coords.mean(axis=0) diff = coords - mean cov = np.cov(diff.T) eigvals, eigvecs = np.linalg.eig(cov) primary_dir = eigvecs[:, np.argmax(eigvals)] angle = np.arctan2(primary_dir[1], primary_dir[0]) return angle, mean这个方向信息后面传给车道保持系统时非常有用,要么计算车辆航向与标线方向的夹角,要么用于车道线拟合。
第二是跨帧跟踪与稳定化。单帧检测会有抖动,有的帧某条标线检测到了,下一帧又丢了,再下一帧又出现。hx3493里我加了一个轻量级的帧间跟踪:基于检测框中心的欧式距离做贪心匹配,给每个标线片段一个短暂ID。匹配上之后,对框的中心位置和置信度做EMA平滑。这个操作不算高明,但效果很实在——输出视频里的框不再闪烁跳动,稳定性明显提升,而且给后续的统计(比如标线破损检测)提供了时间维度信息。
第三是标线状态统计与输出。基于检测结果,可以做两个有价值的统计:一是每条道路的标线破损指数,连续跟踪同一ID的标线片段,如果置信度持续偏低且时有时无,说明这段标线磨损严重;二是黄线实线占比,统计画面中黄色标线像素面积占总标线的比例,可以用于交通设施巡检报告。这块代码不复杂,但要有意识地设计输出数据结构,方便对接上层业务。
4.3 实时视频流接入方案
实际部署时,摄像头输入往往是RTSP视频流,而不是本地mp4文件。Ultralytics的predict接口虽然支持直接传RTSP地址,但解码用的OpenCV在高分辨率长视频场景下会积累延迟,因为OpenCV的VideoCapture缓冲不够智能。我的做法是用独立的读取线程,配合collections.deque做环形缓冲,只保留最新两帧,推理线程总是取出最新帧进行处理。
import cv2 import threading from collections import deque class RtspStream: def __init__(self, url, buffer_size=2): self.cap = cv2.VideoCapture(url) self.buffer = deque(maxlen=buffer_size) self.lock = threading.Lock() self.running = True def start(self): t = threading.Thread(target=self.read_loop, daemon=True) t.start() return self def read_loop(self): while self.running: ret, frame = self.cap.read() if not ret: continue with self.lock: if len(self.buffer) == self.buffer.maxlen: self.buffer.popleft() self.buffer.append(frame)这个方案的延迟控制得很好,实际测试一个1080p的RTSP流,从摄像头画面到输出标注结果,端到端延迟能控制在两帧以内(约66ms),对于道路标线巡检和ADAS辅助来说完全够用。
5. 测试评估与标杆对比
5.1 定量指标与多场景验证
模型训练完成后,除了看mAP,我还做了一套面向真实场景的评估体系。mAP是一个整体指标,但交通标线检测需要知道模型在哪些场景、哪些类别上可用,哪些不可用。我将测试集按场景类型拆分:晴天白天、阴天、雨天、夜间、逆光、隧道内,逐场景统计AP和召回率。
测试结果(yolov8s,1280分辨率):
| 场景 | mAP50 | 召回率@0.35 |
|---|---|---|
| 晴天白天 | 0.931 | 0.904 |
| 阴天 | 0.912 | 0.885 |
| 雨天 | 0.784 | 0.742 |
| 夜间 | 0.673 | 0.658 |
| 逆光 | 0.716 | 0.693 |
| 隧道内 | 0.901 | 0.877 |
夜间和雨天是重灾区,这符合预期。雨夜就更不用说了。针对夜间问题,我的改进方案有两个方向:一是给模型输入做数据层面的适配,把训练集中的图像做亮度降低、增加对比度噪声来模拟夜视效果;二是从感知层面做融合,引入IR摄像头或者摄像头自动曝光模式下的短曝光帧。目前项目hx3493用的还是可见光单目方案,夜间的实测效果能接受但不理想,如果要做到全天候全场景,下一版会考虑多光谱融合。
5.2 不同YOLO家族模型的横向对比
在定了yolov8s作为最终模型之前,我还对比了YOLOv5、YOLOv7和YOLOv8同级别模型,以及YOLOv8不同尺寸的差异。这个对比的意义在于:告诉后来者,模型选型不是看指标越新越好,要看部署约束和数据形态。
| 模型 | 参数量 | 推理耗时(ms) | mAP50 | 备注 |
|---|---|---|---|---|
| YOLOv5s | 7.2M | 8.1 | 0.862 | 需要anchor聚类,不稳定 |
| YOLOv7-tiny | 6.1M | 7.6 | 0.843 | 导出ONNX踩坑多 |
| YOLOv8s | 11.2M | 9.3 | 0.913 | 最终选择 |
| YOLOv8n | 3.2M | 5.8 | 0.871 | 精度略低,速度极快 |
| YOLOv8m | 25.9M | 15.2 | 0.927 | 精度高但嵌入端吃力 |
YOLOv5s的mAP明显低于同级别YOLOv8s,除了模型本身的差距之外,还有anchor带来的问题——标线长宽比跨度大,聚类出的anchor很难同时照顾箭头和车道线。YOLOv7在计算效率上不错,但部署生态没有YOLOv8完整,转TensorRT过程遇到了若干算子兼容问题。最终yolov8s是在精度、速度和部署便利性之间最平衡的选择。
6. 常见问题与排查经验
6.1 远距离标线检测不到怎么办
这是标线检测里收到最多的反馈。远距离标线在图像上占的像素很少,YOLOv8下采样32倍后,特征图上一个点对应原图32x32区域,一个10x10像素的远端标线在深层特征图上几乎无迹可寻。
排查路径,按顺序来:
- 检查输入分辨率是不是太小。很多人训练和推理用的是默认640,如果你用640训练检测箭头,效果一定烂。先把imgsz提到960或1280。
- 检查是否用了多尺度训练。Ultralytics的scale参数默认0.9,训练时会随机缩放图像。但如果你的验证场景中远处标线占比高,放大scale可以覆盖更多尺度。
- 检查数据分布。训练集中近处标线(面积大)样本过多,远处标线(面积小)样本太少,模型整体偏向学习大目标。最简单的办法是统计所有标注框的面积分布,把面积小于32x32的框数量拉开跟大框的比例。
- 考虑加P2检测层。YOLOv8从P3到P5输出,P2是更高分辨率的特征图(原图1/4),对小目标更友好。但P2层计算量大,推理速度会慢30%左右。hx3493这个项目后来在精度瓶颈期尝试过加P2,mAP50涨了0.8个点,但为了保留实时性最终没有用。
还有一个非常关键但很多人不注意的点:远处标线的真实标签长度。一条在图像里占8个像素宽的线,标注时如果把整条可见段全部框进去,那目标框的长宽比非常极端,模型难学。标注时要根据目标在图像中的视觉尺度决定标出多长,距离远的就标注短一些,让框的形状趋于合理。
6.2 虚实线分类混乱如何解决
白色实线、白色虚线、黄色实线、黄色虚线这四类的混淆是标线检测最常见的问题。原因也很朴素:远距离下,虚线的间隙在图像上小于一个像素,视觉上跟实线没有区别;天气干燥时旧标线掉色,黄色变淡接近白色。
我踩过坑并总结出几条有效经验:
不要先分类再检测。一开始我尝试用同一个模型同时输出类别和框,类别是"white_solid"和"white_dashed",结果mAP50只有0.78。后来改为先检测标线(统一输出"marking"类别),再用一个轻量级分类网络在框内判断虚实和颜色。这个两阶段方案让分类准确率提升了7个百分点。检测和分类是不同粒度的任务,强扭在一起反而互相干扰。
利用上下文信息。虚线有周期性特征,相邻两个虚线段的间隙距离是有规律的。在检测结果的后处理中,如果连续相邻的多个框都是白色实线,但它们的间距均匀且间隙宽度接近线宽,可以考虑把这一串改判为虚线,因为真实的连续实线不会出现这种周期性间隙。这个基于规则的修正简单有效,但注意只适用于同一ID跟踪轨迹上的框。
夜间颜色矫正。夜间黄线在路灯下可能偏白,白线可能偏黄,如果纯粹靠颜色判别,误差很大。我在部署时加入了基于位置的先验:路面上黄色标线通常出现在车道分隔线、路缘线等位置,白色标线更常出现在车道边缘和停止线。但这只能是辅助约束,核心还是靠检测数据中的样本均衡。
6.3 误检与漏检典型场景分析
整理几类我在测试阶段遇到的典型误检案例,方便大家对照排查。
第一类是路面接缝误检为白实线。水泥路面的伸缩缝在图像上是笔直的深色线条,但部分接缝两侧会积灰,形成一条浅色线,视觉上和白色标线非常接近。这类误检的普遍特征是:框的高度(垂直于线方向)偏窄,且置信度偏低。我给后处理加了一个几何约束:标线框的宽度(横向)应大于其高度的某比例,否则判定为非标线,这个方法可以过滤掉大量细长条状的伪目标。
第二类是导向箭头与斑马线的混淆。斑马线由多条平行的白色粗短线组成,在透视作用下,远处的斑马线片段形状与一个箭头下部矩形很接近。混淆主要发生在低分辨率输入条件下。我的处理是在后处理阶段引入类别亲和度规则:如果某个框被识别为箭头,但它的空间位置上与其左右一定距离内存在大量同类框(斑马线),则这个框更可能是斑马线的局部片段,将其类别改为斑马线。这个启发式规则在真实测试中能修正10%左右的近处混淆,但在远处还是靠模型本身的分辨能力。
第三类是旧标线残留导致的重复检测。道路翻新时旧的标线没有彻底铲除,新标线画在旁边,形成两套标线。模型会把旧标线也检出来,造成双重检测。目前这个问题的根治难度大,我的缓解方案是跟踪阶段过滤:两条几乎平行且距离很近的标线片段,保留框置信度高的一条。这条规则在快速路上实测效果不错,但在旧线比新线更清晰的路段会留有隐患。
6.4 工程化落地的注意事项
作为一个已经部署运行的项目,hx3493最后落地阶段还有几点工程化经验值得分享:
模型热更新机制。路况会随季节变化,夏天标线清晰,冬天积雪覆盖,春天标线磨损。如果模型一直不更新,效果会逐渐劣化。我搭了一个简单的模型热更新通道:路侧设备每天回传低置信度样本到中心服务器,每周产出一个增量微调模型,通过OTA下发到设备端,加载新权重后热切换。整个流程不中断推理服务。
推理失败保护。现场设备可能遇到视频流断流、解码异常、显存溢出等故障。如果算法进程崩溃但没有守护机制,摄像头就会变成摆设。建议跑一个看门狗线程:如果单帧推理耗时超过设定的告警阈值(比如1000ms),自动重启推理进程,保证服务可用性在99.9%以上。
输出协议统一化。检测结果如果直接输出成JSON,下游业务方解析成本高且容易出错。我更推荐用一套固定的输出协议,例如将检测到的每条标线封装成"类型编号+置信度+中心点坐标+方向角+框坐标"的结构化对象,序列化为JSON或Protobuf。这样下游不管是做统计报表还是做可视化,都能直接根据协议解析,不需要跟算法团队反复对字段含义。
7. 踩坑记录与后续规划
7.1 整个项目中最值的三个解决思路
在这个项目里,走了不少弯路,也留下了一些我觉得特别值得重复使用的解决思路。
第一个是"检测+分类"的两阶段拆解。YOLOv8作为单阶段检测器,让它在同一个输出头里既完成"这是什么"又完成"这在哪",是模型能力的天花板约束。在虚实线分类上,我把"检测是什么"和"判断线型"拆开以后,精度直接上了两个台阶。如果你接手类似的项目,面对的是形态差异极大又需要细分类的目标,这个思路大概率适用。
第二个是透视增强和类别不平衡的组合拳。标线检测的难点不是目标密集,而是同一目标在远近摄像头视角下的形态变化太大,加上类别样本分布天然不均。透视增强解决前者,类别权重和困难样本挖掘解决后者。两件事分开做效果都不明显,组合起来才有效。
第三个是"先量化再剪枝"的顺序调整。大多数人的路线是先剪枝再量化,我一开始也是这么做,但发现剪枝后的模型对量化误差更敏感,精度掉得厉害。后来调整为:先做PTQ量化,再基于量化后的模型做轻量剪枝,并在剪枝后重新小步长微调。这样最终部署模型的精度保留率提升了接近50%。原因是剪枝会改变网络中的重要特征分布,如果先剪枝再量化,量化校准过程就很难找到合适的动态范围。
7.2 后续扩展方向
严格来说,hx3493目前的成果是解决了单目可见光下的标线检测问题,但离一个完整的交通标线巡检系统还有不少距离。
我自己在规划中的几个方向,写出来给大家做参考:
标线磨损评估。当前模型的置信度间接反映标线清晰度,但置信度受光照影响太大。下一版计划在检测的基础上增加磨损分割模块,对标线内部做像素级破损分析,输出磨损率。这需要更精细的标注方式,用多边形而不是框来描述破损区域的边界。
多视角融合检测。单目相机存在视野盲区,尤其在路口,左转待转区、导流岛等区域的标线会被前车遮挡。计划用环视相机系统的多路画面,通过标线在多个视图中的重投影约束实现融合检测,消除单目遮挡。
边缘计算模型加速。目前TensorRT INT8方案还有进一步优化空间,比如使用CUDNN针对卷积的自动调优、算子融合、多stream异步推理,把推理耗时再压到30毫秒以内。这个方向对硬件升级收益立竿见影。
7.3 最后一点个人体会
做交通标线检测这类垂直场景的视觉项目,最容易被低估的其实是数据工程能力。模型训练环节,如果只是跑一下ultralytics默认命令,效果一定平庸;真正拉开差距的是对数据分布的洞察、标注规范的优化、增强策略针对性的调整,这些工作没有炫酷的模型结构创新,但每一步都在切切实实影响最终指标。
hx3493这个项目从开始到现在,我最大的体会是:把通用模型套上垂直场景,最大的工作量在场景适配而不是网络结构。你要理解你所检测的目标在真实世界中如何被光照、透视、磨损和遮挡影响,然后把这个理解翻译成数据标注规则和训练策略。YOLOv8本身只是个执行力很强的工具箱,真正做决策的还是设计这套流程的人。这也是为什么同样用YOLOv8,有的人调的模型表现平平,有的人却能做出稳定的工程化方案。