ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

YOLO虚线车道线检测实战:从数据集解析到训练避坑指南

2026/9/28 5:16:36 拓冰建站 浏览量
YOLO虚线车道线检测实战:从数据集解析到训练避坑指南 简介面向车道线检测与目标检测任务这份YOLO系列数据集包含1659张已标注图像配套完整标签与训练配置可直接用于YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等主流版本模型的训练、验证与测试。压缩包共2000个文件其中1161个XMLVOC格式与839个TXTYOLO格式标签分别存放在独立文件夹TXT严格采用 x_center y_center 归一化格式XML则便于在LabelImg等标注工具中查看或二次调整同时提供data.yaml配置文件省去手动划分数据集与格式转换的步骤。数据已按训练需求完成划分图像与标签同名一一对应目录结构清晰适合道路交通场景下的车道线、虚线检测项目快速迭代。包体仅74.72MB轻量易下载。目前已有79人学习使用对希望直接获得高质量标注数据、跳过数据准备环节的开发者而言是一份即拿即用的实用资源可直接投入模型训练与效果验证。1. 用 YOLO 算法做虚线车道线检测这个数据集的工程定位用 YOLO 算法做车道线检测多数人第一反应是上分割模型把每个像素都标成车道线。但这个数据集走的是另一条路把虚线车道线当成一个个“虚线段目标”用边界框直接框出来1659 张图像带标签标注格式是 YOLO 的 txt。它的价值不在像素级精度而在速度快、模型小、能直接回答“这条虚线断了几处、还剩多少段”。做公路巡检、车道线磨损评估、自动驾驶前视感知的工程人员用这类数据训练出来的是目标检测器不是分割头。这种折中做法在工程里很常见分割模型对虚线断点敏感但标注成本高、推理重关键点模型又要额外做点匹配。边界框方案把虚线检测转化成“找每一个可见的短线段”训练和部署链路最成熟。如果你要判断车道线虚实、统计虚线数量、或者给后处理提供线种子点这个方向值得先跑通。要说清楚的是这套方案不追求“线宽”和“像素边界”这种精细度它追求的是召回率和可数性。看清楚这一点后面调参数才不会跑偏。2. 拆开车道.zip目录结构、YOLO 标签和 data.yaml 怎么读2.1 压缩包里应该有什么images、labels、data.yaml 三件套拿到一个 YOLO 格式的数据集名字叫“车道.zip”也好、“yolo算法-车道线-虚线检测道数据集”也好解压之前先用unzip -l看一眼别急着全部解开:unzip -l 车道.zip | head -40这条命令只列压缩包内容列表不实际解压。主要看三样东西:有没有images/、labels/目录以及根目录有没有data.yaml。如果这几样都在这就是标准的 YOLO 数据集结构。解压时我习惯单独放到一个干净目录unzip 车道.zip -d ./lane_data cd lane_data tree -L 2正常的目录结构一般长这样lane_data/ ├── data.yaml ├── README.roboflow.txt ├── images/ │ ├── train/ │ ├── valid/ │ └── test/ ├── labels/ │ ├── train/ │ ├── valid/ │ └── test/ └── classes.txtREADME.roboflow.txt和classes.txt是导出工具留下的附加文件不是训练必需。data.yaml才是训练时真正读的配置。如果看到的是_annotations.coco.json而不是labels/说明压缩包导出成了 COCO 格式需要先转回 YOLO不在本数据集讨论范围内。这里有一个容易忽略的点images和labels的目录名要严格对应images/train里有多少张图labels/train里就要有多少个同名.txt文件。少一个训练时会报“label file missing”或者干脆把那张图过滤掉。2.2 YOLO 标签的五个数字类别、中心点、宽高打开任意一个.txt标签内容是一行五个数字例如0 0.512 0.303 0.062 0.118含义是类别id 中心点x 中心点y 宽度w 高度h后四个值全部经过归一化除以了图像宽和高。也就是说cx0.512表示框中心在图像宽度 51.2% 的位置w0.062表示框宽度占图像宽度的 6.2%。做车道线检测这个格式下有个明显特点虚线段框的宽高比通常很极端要么是横向的短条要么是顺着消失点方向的斜长条。和人、车这类接近方形的目标完全不同。如果你在标签里看到大量宽高比接近 1 的框那可能是标注口径有问题——把虚线的一个断点画成了方形区域或者把路面污渍也框了进去。另外标签行数就是这张图里的目标数量。虚线检测场景下一条车道线在近处可能有 35 个完整虚线段远处还有 23 个小线段所以单张图的框数通常不多。如果某张图有几十个框要么是场景里车道线特别密集要么是重复标注了。2.3 用一段脚本统计 1659 张图的标签分布解压之后先统计不要直接开训。我一般用这个脚本快速摸底from pathlib import Path from collections import Counter def scan_labels(label_root, img_w1280, img_h720): stats { label_files: 0, boxes: 0, classes: Counter(), wh_ratio: Counter(), tiny_boxes: 0, bad_lines: [], } for txt in sorted(Path(label_root).rglob(*.txt)): if txt.stem classes: # 跳过 classes.txt continue stats[label_files] 1 for line_no, line in enumerate(txt.read_text().strip().splitlines(), 1): if not line.strip(): continue parts line.split() if len(parts) ! 5: stats[bad_lines].append(f{txt}:{line_no} 字段数{len(parts)}) continue try: cls, cx, cy, w, h map(float, parts) except ValueError: stats[bad_lines].append(f{txt}:{line_no} 数值错误) continue stats[boxes] 1 stats[classes][int(cls)] 1 # 宽高比限制在 0.1~10.0超出的集中显示 ratio float(w) / max(float(h), 1e-6) stats[wh_ratio][round(min(ratio, 10.0), 1)] 1 # 小框判定宽或高换算回像素后小于 8px if float(w) * img_w 8 or float(h) * img_h 8: stats[tiny_boxes] 1 return stats for split in [train, valid, test]: s scan_labels(flabels/{split}) print(split, 标签文件:, s[label_files], 框总数:, s[boxes]) print( 类别分布:, dict(s[classes])) print( 小框数量:, s[tiny_boxes]) if s[bad_lines]: print( 异常行示例:, s[bad_lines][:5])这段脚本做三件事统计每个 split 的标注文件数、统计每个类别的框数量、挑出异常行和过小的框。img_w和img_h参数要换成数据集的真实图像尺寸。不确定的话用 PIL 或 OpenCV 随机读 5 张图拿尺寸不要像我示例里硬编码 1280x720。通过这个脚本你能快速判断数据集到底标了几个类、类别数量是否均衡、小目标占比有多大。小框数量如果超过总框数 30%后面训练就必须把输入分辨率提高或者对标注重新做处理。这一步花十分钟能避免训练完才发现数据问题。3. 把「虚线检测」翻译成 YOLO 训练最小可跑通方案3.1 先定检测单位虚线段还是整条线训练前要明确一个语义问题模型输出的“一个目标”到底是什么。虚线车道线在物理世界是一条断续的白线落到图像上是一段段短线段。如果标注是按“每个可见的虚线段”画框那么模型学的是段级别的目标检测输出框密集后处理需要再把同一条线上的框连起来。如果标注是“整条虚线车道线”画一个大框那么框会非常大而且形状和实线框几乎无法区分模型很难学。从标题“虚线检测”和 1659 张图像带标签的组合来看这种数据集更常见的是前者虚线段级别的检测。解压后你可以验证看几张训练图的标注框如果一张图里同一条虚线附近有多个小框那就是段级别标注。这个选择是合理的。整条虚线识别用分类网络加语义分割更自然用边界框去框一整条虚线反而丢了断点信息。段级别的框保留了“这里有一段虚线段”的局部证据后处理阶段再聚合灵活度高很多。3.2 data.yaml 怎么写给虚线检测YOLOv8 训练时只认data.yaml。把它放在lane_data/根目录下内容按解压后的实际目录写# 数据集根目录配置 path: ./lane_data train: images/train val: images/valid test: images/test # 类别数量与名称以 labels 目录里实际统计为准 nc: 1 names: 0: dashed-lanepath字段建议用相对路径并保证yolo命令的当前工作目录在lane_data的上一级。用绝对路径虽然能跑但换机器、换服务器后要改文件容易被坑。nc和names必须和标签文件里的类别 id 对应。如果第 2.3 节统计出来有两个类别这里就要写nc: 2 names: 0: dashed-lane 1: solid-lane类别的顺序无所谓但 id 不能乱。YOLO 标签文件里的0永远对应names里第一项1对应第二项。如果数据里是虚线、实线两类而你只写了nc: 1训练时会直接报“class id out of range”。3.3 YOLOv8 训练命令与起步参数我一般用 YOLOv8n 起步跑通流程再换大模型。训练命令yolo detect train \ data./lane_data/data.yaml \ modelyolov8n.pt \ imgsz1280 \ epochs100 \ batch16 \ lr00.005 \ patience15 \ augmentTrue \ device0说几个关键参数。imgsz1280是这个数据集里最重要的参数之一。车道线虚线段在 50 米开外只有十几个像素如果按默认 640 训练模型看到的远景部分几乎淹没在背景里。把输入分辨率提到 1280等于是把小目标放大了两倍。显存允许的话用 1536 都能接受。epochs100对 1659 张图来说已经偏多所以加了patience15早停。训练集规模小模型很容易在 50 个 epoch 后进入过拟合不看验证集 loss 硬训完 100 轮没有意义。lr00.005比默认的 0.01 低配合预训练权重做迁移学习。小数据集上学习率太高会把预训练特征冲掉尤其对虚线段这种高频纹理特征。batch16在 8GB 显存、imgsz1280 下比较稳妥。单卡 V100 或 T4 可以加到 32但要观察显存占用。模型选择可以参考这张表模型参数量级适用场景对这个数据集的建议YOLOv8n约 3.2M原型验证、端侧部署先跑通流程、看数据质量YOLOv8s约 11.2M精度与速度均衡默认推荐漏检多时换它YOLOv8m约 25.9M追求精度显存充裕、后处理较弱时用不要一上来就用最大的模型。1659 张图像撑不起太大的网络模型容量上去了但训练数据不够结果往往比小模型还差。先把 v8n 的训练结果可视化如果漏检集中在远景优先调分辨率和数据增强而不是换大模型。4. 动手训练前先清一遍数据划分与质量检查4.1 划分 train/val/test别让同一条路串组如果解压后已经有train/valid/test三分目录直接用如果没有需要自己划分时注意一个关键问题不要逐帧随机划分。行车记录仪或道路采集视频抽出来的图像相邻帧之间场景高度重复。随机按文件洗牌划分会让同一段路的相似帧同时出现在 train 和 val 里。训练时模型相当于见过验证集内容val 指标虚高部署到新路段就翻车。正确做法是按序列划分。假设文件名是seq001_000123.jpg这种带序列前缀的格式from pathlib import Path import random root Path(./lane_data) src root / images seq_to_images {} for img in src.glob(*.jpg): seq img.stem.split(_)[0] # 取序列前缀 seq_to_images.setdefault(seq, []).append(img) seqs list(seq_to_images.keys()) random.seed(42) random.shuffle(seqs) n len(seqs) train_seqs set(seqs[:int(n * 0.8)]) val_seqs set(seqs[int(n * 0.8):int(n * 0.9)]) test_seqs set(seqs[int(n * 0.9):])之后再按划分结果把图像和对应的同名标签文件移动或复制到images/train、labels/train等目录。判断序列前缀只是示例实际文件名如果完全没有规律那就按采集时间戳排序后按时间窗口划分效果一样。划分完成后再跑一遍 2.3 的统计脚本确认 train/val/test 三个集合里类别分布接近。如果 val 里恰好全是白天、train 里全是夜间这个划分无效重新分。4.2 用脚本筛出坏框过小、越界、宽高比异常数据集不是从标注平台出来后就能直接喂给 YOLO 的。我见过不少“带标签”数据集框本身就有问题。检查脚本from pathlib import Path def find_bad_labels(labels_root, img_w, img_h, min_size8): bad [] for txt in sorted(Path(labels_root).rglob(*.txt)): if txt.stem classes: continue for line_no, line in enumerate(txt.read_text().strip().splitlines(), 1): if not line.strip(): continue parts line.split() if len(parts) ! 5: bad.append(f{txt}:{line_no} 字段数错误) continue try: cls, cx, cy, w, h map(float, parts) except ValueError: bad.append(f{txt}:{line_no} 非数值内容) continue x1 (cx - w / 2) * img_w y1 (cy - h / 2) * img_h x2 (cx w / 2) * img_w y2 (cy h / 2) * img_h # 越界框框的一部分落在图像外 if x1 -1 or y1 -1 or x2 img_w 1 or y2 img_h 1: bad.append(f{txt}:{line_no} 越界 x1{x1:.1f} y1{y1:.1f} x2{x2:.1f} y2{y2:.1f}) # 过小框宽或高小于 min_size 像素 if w * img_w min_size or h * img_h min_size: bad.append(f{txt}:{line_no} 小框 {w*img_w:.1f}x{h*img_h:.1f}px) return bad bad find_bad_labels(labels/train, img_w1280, img_h720) print(坏框数量:, len(bad)) for item in bad[:30]: print(item)越界框处理要谨慎。框只超出图像边界 1~2 个像素说明是标注浮点误差问题不大超出 5% 以上说明坐标换算有问题需要修。小框则是另一个逻辑。如果w*img_w 2这个目标在 1280 分辨率下只有 2 像素宽人眼都很难确认是什么模型更学不出来。这类框建议直接删掉保留反而制造噪声。但要注意虚线远景的小框会占不小比例设置min_size8只是筛查基线真实阈值根据你部署时希望检测的最远距离来定。宽高比异常的框我单独看。虚线段框宽高比偏离同类均值太多要么是标注拖拽误差要么是框错目标。建议把宽高比最大的 50 个框对应的图片裁出来看一眼比改代码更直接。4.3 类别不均衡先看统计再决定要不要重采样第 2.3 节的统计结果里如果classes分布是两个类别且差距超过 5 倍就需要处理。比如实线框 8000 个、虚线框 1200 个模型会偏向学实线虚线漏检率明显更高。常见做法有两种。第一种是重采样把少数类样本复制几份到训练目录让模型每个 epoch 都能看到足够多虚线框。复制的是标签文件和对应图像不改变原始坐标。要注意复制后 train 里同一张图出现多次会增加训练时间但不会造成验证集污染。第二种是放弃多分类只保留“虚线”一个类别。如果你的应用目标就是识别虚线段没必要让模型在实线/虚线之间做区分。实线框在训练里只会干扰模型把类别合并成单类后模型专注找所有短线段实线在推理时会被过滤掉——因为实线是一个长条不会同时框出很多短线。“软标签”在这个场景里不太实用YOLO 的边界框分类逻辑就是硬类别。与其在损失函数里折腾标签平滑不如把类别语义定清楚让标注一致性上去效果来得直接。5. 虚线检测避坑指南小目标、断点与标注不统一5.1 远处虚线段只有十几个像素漏检到怀疑模型现象近处 0~30 米虚线段检测很准30 米以外时有时无。验证集 mAP 看起来能用一到实车视频上远景部分基本全漏。原因虚线段本身就是细小目标在 1280x720 的分辨率下30 米外的虚线可能只有 10~20 像素宽、30~50 像素长。YOLO 的检测头设计对大物体和小物体天然不公平主干网络下采样几轮后小目标在特征图里只剩一点点响应。再加上训练数据增强默认会把图像随机缩放小目标一缩可能只剩几个像素模型根本学不到有效特征。解决第一把imgsz从 640 提到 1280甚至 1536让小目标在输入图像里占据更多像素。第二调整数据增强把scale0.5而不是默认的 0.9限制训练时图像被过度缩小mosaic增强对小目标并不友好它会把四张图拼在一起原本就小的虚线段被进一步切碎训练后期可以关闭。第三如果线上环境允许推理时做重叠切片把图像切成 640x640 的块分别检测再合并结果小目标召回会明显提升。代价是推理时间和显存占用增加。先试前两种切片是最后的后悔药。5.2 虚线被拆成碎片一条线变成几十个框现象检测结果里一条虚线车道线被框出二三十个互相重叠的框而且框之间缝隙很大看起来像碎了一地。原因标注口径不统一。一部分虚线段被标注成一个框另一部分可能被标成两个框还有压缩包里的标签如果经历过自动标注或数据增强重复框也会残留。框的尺寸、位置、数量都不稳定模型学的目标就乱了。解决先做标签清洗。用脚本找出同一张图里 IoU 大于 0.5 的重复框保留置信度最高的一个。再做后处理合并推理完成后按框中心点的横向位置聚类把落在同一条车道线上的框归为一组然后纵向合并。合并时判断条件是两个框的横向中心距离小于阈值且纵向间隔小于虚线段典型间距的两倍就视为同一条线的相邻段。这个阈值一般取图像宽度的 2%~3%具体看摄像头安装角度。不要在模型层面试图解决这个问题模型只会把多种标注形态当成多模态学进去后处理合并更可控。5.3 实线当成虚线检白天检黑夜丢现象实线车道线频繁被识别成虚线同一模型白天效果不错傍晚或夜间漏检大增。原因如果数据集只标了虚线一个类别实线和虚线在局部纹理上高度相似模型只能靠上下文判断。白天光照好虚线断点清晰模型能区分夜间对比度下降虚线断点看不清模型只能通过“这里有白色条状物”来判断实线自然就成了误报来源。解决把实线也标注出来即使你的业务不需要实线让模型多学一个负类别能显著减少误报。这是我在做车道线相关项目时的实测结论单类检测器在这种视觉混淆场景里很容易翻车。夜间问题则要靠数据本身解决增加夜间样本或在训练增强里把亮度、对比度扰动加大让模型不过度依赖“白色高亮”这个特征。调置信度阈值也能缓解一部分误报但治标不治本会把真正的虚线也滤掉。5.4 混淆矩阵总和看起来不对先查评测脚本现象训练完看混淆矩阵每一行的总和不是 1行和列加起来都对不上网上常有人吐槽“yolo混淆矩阵总合不唯一”。原因这不是模型问题是评测脚本的可视化逻辑。Ultralytics 绘制的混淆矩阵每个类别单独做了行归一化显示的是“该类别的真实样本被预测成各类别的比例”所以行和是 1。如果你去看列方向的总和或者去对比绝对数量自然对不上。解决看行不看列。每一行代表一个真实类别行内最大的那个值就是该类别的召回率。类别不平衡时混淆矩阵比 mAP 更能说明问题如果实线误检成虚线的比例超过 10%那就按 5.3 的做法处理类别如果少数类召回率连 50% 都不到优先看是不是小目标问题。不要在这个细节上浪费时间数据问题排查完了这个数字自然就正常。6. 把一排检测框拼回「虚线」坐标后处理与验收技巧6.1 框中心点排序 二次多项式拟合从框回到线检测模型输出的是一个个框但对车道线应用来说下游要的是“完整的一条虚线”。我用一个简单的后处理把框聚合成线import numpy as np # preds 是单帧检测结果每项 [x1, y1, x2, y2, conf, cls] def boxes_to_lines(preds, lane_width_thresh30): centers [] for x1, y1, x2, y2, conf, cls in preds: centers.append(((x1 x2) / 2, (y1 y2) / 2, conf, cls)) # 按纵坐标排序从图像近处到远处 centers.sort(keylambda c: c[1]) lines [] for cx, cy, conf, cls in centers: matched False for line in lines: # 横向距离小于阈值视为同一条车道线 if abs(cx - line[center_x]) lane_width_thresh: line[points].append((cx, cy)) line[center_x] np.mean([p[0] for p in line[points]]) matched True break if not matched: lines.append({points: [(cx, cy)], center_x: cx}) fitted [] for line in lines: pts line[points] if len(pts) 3: continue # 二次多项式拟合图像平面上车道线投影近似抛物线 coeffs np.polyfit([p[0] for p in pts], [p[1] for p in pts], 2) fitted.append((pts, coeffs)) return fitted排序放在聚类之前是为了让聚类对横向抖动不敏感。同一个虚线段检测框横坐标可能偏差几个像素按纵坐标排序后相邻框更可能落在同一条线的聚类区间内。二次多项式拟合是因为车载前视摄像头下车道线在图像平面上的投影基本是抛物线。三次多项式虽然拟合更灵活但点数少时容易过度弯曲虚线段本来就稀疏二次是稳定选择。6.2 验收时除了 mAP 还看什么mAP50 能说明“检没检出来”但虚线检测这个任务大家真正关心的是虚线段数量和断点位置。我建议再做两件事。第一在测试集上统计虚线段召回率只看“模型检出的虚线段数量”除以“人工标注的虚线段数量”按距离分段看比如 0~20 米、20~40 米、40 米以上。如果远程段召回明显下降回去调imgsz和增强参数。第二抽 10 段典型视频白天、夜间、逆光、雨天各两段人工数每一帧的虚线可见段数量和模型输出对比误差超过 20% 就说明数据或后处理还有问题。指标再好看不落到真实场景数量上都是虚的。6.3 我的收尾习惯我现在的固定流程是v8n 起步imgsz1280先跑通用一段视频看漏检集中在哪个距离段再决定要不要换 v8s 或调增强。模型训练完成后把导出和推理也一起验证yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcetest_video.mp4 \ conf0.25 \ iou0.5 \ save_txt \ save_conf导出时如果部署在边缘设备就转成 ONNX 或半精度 TensorRT避免推理端和训练端行为不一致的玄学问题。最后再强调一次先确认标注口径再做数据清洗最后才调模型。顺序反了后面所有调参都是在给脏数据打补丁。希望这些踩过的坑能帮你少走几步弯路。本文还有配套的精品资源点击获取