1449张道路标线数据集实战:从标注检查到YOLO训练与SAHI推理
简介这份资源面向计算机视觉与自动驾驶方向的学习者和研究者提供一套已完成标注的道路标线识别数据集可用于目标检测模型的训练、验证与性能评估。数据集共1449个文件以png图像和txt标注文件为主另含少量m脚本文件压缩包约250.25MB图像与标注一一对应便于直接接入YOLO、Faster R-CNN等检测框架或U-Net类分割网络。标注覆盖实线、虚线、双黄线、停车线等常见标线类型适合开展像素级分割、边界框定位与类别分类等任务。已有1122人学习下载可作为自动驾驶感知、智能交通系统研究的练手与对比基准。使用者可据此划分训练集、验证集与测试集结合随机翻转、旋转、缩放等数据增强手段提升模型鲁棒性并以mAP、IoU、准确率、召回率等指标衡量效果快速搭建从数据到评估的完整实验流程。1. 1449张道路标线数据集从拿到标注文件到跑通第一个检测模型你手里有一份1449张的道路标线数据集标注已经完成格式大概率是Pascal VOC的XML或者YOLO的TXT。问题从来不是“有没有数据”而是“这批数据能不能直接喂给模型、喂进去之后mAP能不能看”。道路标线识别这个任务和通用目标检测有本质区别标线是细长、低对比度、强透视变形的目标车道线、斑马线、导向箭头、停止线这几类的类间差异极大而类内差异又极小——同一个箭头在近处和远处像素面积能差几十倍。1449张这个量级说多不多说少不少刚好卡在“从头训不够、微调够用”的边界上。这篇文章面向的是已经拿到这批数据、准备用YOLO系列或者类似框架跑一版baseline的工程师我会把从数据检查、格式转换、增强策略到训练参数、踩坑排查的完整路径拆开讲每一步都落到可执行的命令和代码上。2. 先搞清楚你的1449张标注到底能不能用2.1 道路标线检测和通用目标检测的四个关键差异很多人拿到标注文件第一反应是直接转格式开训结果训到一半发现某些类别AP始终为0回头查才发现标注本身就有问题。道路标线这个任务有几个特性会直接影响你的数据处理策略。第一长宽比极端。一条车道线的标注框可能是800×30像素长宽比超过25:1。YOLO的锚框默认聚类是基于COCO的直接拿来用会导致正样本匹配极少回归损失很难降下去。你需要重新对标注框做k-means聚类或者改用anchor-free的检测头。第二类别不平衡严重。1449张图里车道线可能出现在90%以上的图中但导向箭头可能只有15%的图里有停止线更少。如果按类别统计实例数差距可能到几十倍。这直接影响损失函数的权重设置和采样策略。第三标注框重叠率高。在路口场景中多条车道线汇聚标注框之间IoU可能超过0.3NMS阈值设不好就会互相抑制。常见做法是把NMS IoU阈值从默认的0.45调到0.55甚至0.6但调太高又会引入重复检测。第四小目标占比大。远处标线的像素面积可能只有十几×几像素下采样32倍之后特征图上几乎消失。这是道路标线检测mAP上不去的核心原因之一后面讲增强和特征金字塔时会展开。2.2 用脚本做标注质量体检五个必查项在转格式之前先跑一遍数据体检。下面这段代码检查标注文件的基本健康度包括类别分布、框尺寸分布、越界框和零面积框。import os import xml.etree.ElementTree as ET from collections import Counter import numpy as np def audit_voc_annotations(anno_dir, img_dir): 体检VOC格式标注类别分布、框尺寸、越界、零面积 anno_dir: XML标注文件夹 img_dir: 对应图片文件夹 class_counter Counter() widths, heights [], [] issues {zero_area: [], out_of_bound: [], no_object: [], missing_img: []} for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, xml_file)) root tree.getroot() img_name root.find(filename).text img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): issues[missing_img].append(xml_file) continue # 读取图片尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) objects root.findall(object) if len(objects) 0: issues[no_object].append(xml_file) continue for obj in objects: cls_name obj.find(name).text class_counter[cls_name] 1 bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) w, h x2 - x1, y2 - y1 if w 0 or h 0: issues[zero_area].append((xml_file, cls_name)) if x1 0 or y1 0 or x2 img_w or y2 img_h: issues[out_of_bound].append((xml_file, cls_name)) widths.append(w) heights.append(h) print( 类别分布 ) for cls, cnt in class_counter.most_common(): print(f {cls}: {cnt}) print(f\n 框尺寸统计 ) print(f 宽度: min{np.min(widths):.1f}, max{np.max(widths):.1f}, median{np.median(widths):.1f}) print(f 高度: min{np.min(heights):.1f}, max{np.max(heights):.1f}, median{np.median(heights):.1f}) print(f 长宽比10的框占比: {np.mean(np.array(widths)/np.maximum(np.array(heights),1) 10)*100:.1f}%) print(f\n 问题汇总 ) for k, v in issues.items(): print(f {k}: {len(v)} 个) return class_counter, issues # 调用示例 audit_voc_annotations(./annotations, ./images)这段脚本输出四个关键信息类别实例数分布告诉你哪些类需要过采样或加权框尺寸的中位数和长宽比分布决定锚框怎么设越界框和零面积框必须在训练前修掉否则会导致loss NaN。我一般会重点看长宽比10的框占比如果超过30%基本可以放弃默认锚框直接上anchor-free方案。2.3 从VOC到YOLO格式转换脚本与三个边界坑确认标注质量没问题后转成YOLO需要的TXT格式。转换逻辑本身不复杂但有几个边界情况容易翻车。import os import xml.etree.ElementTree as ET def voc_to_yolo(anno_dir, output_dir, class_list): VOC XML - YOLO TXT class_list: 类别名列表顺序即类别ID os.makedirs(output_dir, exist_okTrue) cls_to_id {name: i for i, name in enumerate(class_list)} for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, xml_file)) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): cls_name obj.find(name).text.strip() if cls_name not in cls_to_id: continue # 跳过不在类别表里的标注 cls_id cls_to_id[cls_name] bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 坑1坐标裁剪到图片范围内 x1 max(0, min(x1, img_w)) y1 max(0, min(y1, img_h)) x2 max(0, min(x2, img_w)) y2 max(0, min(y2, img_h)) # 坑2零面积框跳过 if x2 x1 or y2 y1: continue # 归一化为中心点宽高 cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h # 坑3归一化后坐标必须在(0,1)之间 cx max(0.0, min(1.0, cx)) cy max(0.0, min(1.0, cy)) w max(0.0, min(1.0, w)) h max(0.0, min(1.0, h)) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) txt_name xml_file.replace(.xml, .txt) with open(os.path.join(output_dir, txt_name), w) as f: f.write(\n.join(lines)) # 调用假设你的类别是这四类 voc_to_yolo(./annotations, ./labels, [lane_line, zebra_crossing, arrow, stop_line])三个坑分别是坐标越界有些标注员会把框拉到图片外面、零面积框x2x1的情况、归一化后超出0到1范围浮点精度问题。这三个问题不处理训练时dataloader会直接报错或者产生异常梯度。转换完之后建议再写一个校验脚本逐行检查TXT文件里每行是否有5个字段、坐标是否在0到1之间。3. 训练策略1449张图怎么把mAP拉上去3.1 锚框重聚类用你的数据算出该设多大的框如果你用的是YOLOv5/v8这类基于锚框的框架第一步就是用自己的标注重新聚类锚框。COCO的默认锚框在道路标线场景下匹配度很差尤其是那些长宽比超过20的框。import numpy as np from sklearn.cluster import KMeans def cluster_anchors(label_dir, img_size640, n_anchors9): 对YOLO格式标注做k-means聚类得到适配的锚框 label_dir: YOLO TXT文件夹 img_size: 训练输入尺寸 n_anchors: 锚框数量YOLOv5默认9个 boxes [] for txt_file in os.listdir(label_dir): if not txt_file.endswith(.txt): continue with open(os.path.join(label_dir, txt_file)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue _, cx, cy, w, h map(float, parts) # 还原到像素尺度 boxes.append([w * img_size, h * img_size]) boxes np.array(boxes) print(f总框数: {len(boxes)}) # k-means聚类 kmeans KMeans(n_clustersn_anchors, random_state42, n_init10) kmeans.fit(boxes) anchors kmeans.cluster_centers_ # 按面积排序 areas anchors[:, 0] * anchors[:, 1] sorted_idx np.argsort(areas) anchors anchors[sorted_idx] print(聚类锚框宽, 高:) for a in anchors: print(f {a[0]:.1f}, {a[1]:.1f} (长宽比: {a[0]/a[1]:.2f})) return anchors cluster_anchors(./labels, img_size640, n_anchors9)聚类完把结果写进模型的配置文件里。注意一点如果你的长宽比分布里极端值很多k-means可能会被少数超大框带偏建议先对框做一次过滤去掉面积超过图片面积50%的异常框再聚类。3.2 针对细长标线的增强策略Mosaic要改Copy-Paste要加YOLO默认的Mosaic增强对道路标线不一定友好。Mosaic把四张图拼成一张拼接边缘会产生不自然的截断对于车道线这种连续长条目标截断后的标注框可能只包含一小段线模型学到的特征不完整。我的做法是降低Mosaic的使用概率从默认的1.0降到0.5同时加上针对性的增强。import cv2 import numpy as np import random def copy_paste_augment(img, labels, img_size640, paste_prob0.3): 简化版Copy-Paste增强随机复制图中的标线实例到其他位置 img: 原始图片 (H, W, 3) labels: YOLO格式标注列表 [[cls, cx, cy, w, h], ...] paste_prob: 执行paste的概率 if random.random() paste_prob or len(labels) 0: return img, labels h, w img.shape[:2] new_labels list(labels) # 随机选一个实例 idx random.randint(0, len(labels) - 1) cls, cx, cy, bw, bh labels[idx] # 还原像素坐标 x1 int((cx - bw/2) * w) y1 int((cy - bh/2) * h) x2 int((cx bw/2) * w) y2 int((cy bh/2) * h) x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) if x2 x1 or y2 y1: return img, labels patch img[y1:y2, x1:x2].copy() ph, pw patch.shape[:2] # 随机粘贴位置确保不越界 if w - pw 0 or h - ph 0: return img, labels new_x random.randint(0, w - pw) new_y random.randint(0, h - ph) img[new_y:new_yph, new_x:new_xpw] patch # 添加新标注 new_cx (new_x pw/2) / w new_cy (new_y ph/2) / h new_bw pw / w new_bh ph / h new_labels.append([cls, new_cx, new_cy, new_bw, new_bh]) return img, new_labelsCopy-Paste对小样本类别比如导向箭头、停止线特别有效相当于在不引入外部数据的情况下增加了稀有类别的实例数。但要注意粘贴位置不能覆盖已有标注的关键区域否则会产生标注冲突。实际用的时候我一般只对实例数最少的两个类别做Copy-Paste概率控制在0.3左右。另外针对细长目标随机旋转增强比随机裁剪更有用。道路标线在透视变换下角度变化很大加一个±15度的随机旋转能显著提升模型对斜向标线的召回。3.3 训练参数怎么设从学习率到NMS的完整配置1449张图的量级我一般用YOLOv8s或者YOLOv5s作为baseline参数量在7M左右单卡就能跑。下面是一份经过验证的配置起点。参数建议值说明epochs2001449张图通常100轮左右收敛200轮留足早停空间batch_size16单卡16G显存下YOLOv8s640可以跑16初始学习率0.01SGD优化器cosine调度最终学习率0.0001衰减到初始值的1%warmup_epochs3前3轮线性预热weight_decay0.0005标准值数据量小可以适当加大Mosaic概率0.5从默认1.0降低旋转角度±15度针对透视变形NMS IoU0.55从默认0.45提高减少密集标线互抑置信度阈值0.25推理时先用0.25看召回再调训练命令以YOLOv8为例yolo detect train \ dataroad_marking.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ warmup_epochs3 \ weight_decay0.0005 \ mosaic0.5 \ degrees15 \ iou0.55 \ conf0.25 \ projectruns/road_marking \ namebaseline_v1data配置文件里注意类别数量和名称要和你的标注一致# road_marking.yaml path: ./dataset train: images/train val: images/val nc: 4 names: [lane_line, zebra_crossing, arrow, stop_line]训练过程中重点看三个指标box_loss是否稳定下降、mAP0.5在第几轮开始 plateau、每个类别的AP曲线是否分化。如果某个类别AP一直上不去大概率是实例数太少或者标注质量有问题回到第2章的体检脚本去查。4. 避坑与排查道路标线检测训练中最容易翻车的五个地方4.1 现象mAP0.5卡在0.3上不去loss也不降原因锚框和实际框尺寸严重不匹配。COCO默认锚框的最小尺寸是10×13但你的远处标线可能只有8×3像素匹配不上任何锚框这些目标全部被当成背景。解决跑3.1节的聚类脚本把锚框换成自己数据的。如果聚类后发现最小锚框还是偏大考虑把输入尺寸从640提到1024或者改用YOLOv8的anchor-free模式。4.2 现象训练loss正常下降但验证集mAP波动极大每次跑结果差很多原因1449张图划分训练集和验证集时如果随机划分可能某些稀有类别在验证集里一个实例都没有导致该类AP为0或NaN拉低整体mAP。解决按类别分层采样划分数据集确保每个类别在训练集和验证集中都有足够实例。验证集至少占15%且每个类别在验证集中不少于20个实例。如果某个类别总实例数不到50考虑把它合并到相近类别或者用Copy-Paste增强。4.3 现象模型在近处标线上检测很好远处标线全部漏检原因下采样倍率太高远处小目标在P3特征图上只有1到2个像素经过多次卷积后特征消失。这是FPN结构在细长小目标上的固有缺陷。解决三个方向。一是提高输入分辨率到1024或1280二是在FPN中增加一个更高分辨率的检测头P2层但这会显著增加计算量三是用SAHI切片推理把大图切成小图分别检测再合并推理阶段就能改善小目标召回。4.4 现象密集车道线区域出现大量重复检测框NMS删不干净原因车道线标注框之间高度重叠NMS的IoU阈值设低了会误删正确框设高了又删不掉重复框。而且标准NMS是类别无关的不同类别的框如果重叠也会互相抑制。解决把NMS改成类别内NMS每个类别独立做抑制。YOLOv8默认就是类别内NMS如果你用的是其他框架检查一下这个配置。另外可以试试Soft-NMS对重叠框不是直接删除而是降低置信度在密集场景下效果更好。4.5 现象训练到后期loss突然变成NaN原因学习率太大加上标注中有零面积框或越界框产生异常梯度。1449张图里只要有一两个这样的框就足以在某个batch里把梯度炸掉。解决回到2.2节的体检脚本把所有零面积框和越界框修掉。同时在训练配置里加上梯度裁剪YOLO默认有grad_clip_norm10.0检查一下是否生效。如果已经加了裁剪还是NaN把初始学习率从0.01降到0.005再试。5. 用SAHI切片推理把远处标线的召回再拉一截训练完之后如果远处小目标的召回还是不满意先别急着换模型或者加数据。有一个推理阶段的技巧能在不改模型的前提下把小目标AP提升5到10个点就是用SAHI做切片推理。原理很简单把一张大图切成有重叠的小图每张小图单独送进模型检测最后把结果映射回原图做NMS合并。这样远处标线在切片图里就变成了近处目标像素面积大了好几倍。from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction # 加载训练好的YOLOv8模型 detection_model AutoDetectionModel.from_pretrained( model_typeyolov8, model_pathruns/road_marking/baseline_v1/weights/best.pt, confidence_threshold0.25, devicecuda:0 ) # 切片推理切片尺寸640重叠比例0.2 result get_sliced_prediction( test_image.jpg, detection_model, slice_height640, slice_width640, overlap_height_ratio0.2, overlap_width_ratio0.2 ) # 导出结果 result.export_visuals(export_diroutput/)三个参数需要根据你的图片分辨率调slice_height和slice_width一般设成和训练输入尺寸一致overlap比例设0.2到0.3之间太小会在切片边缘漏检太大则推理时间翻倍。我一般会先用标准推理跑一遍验证集记录每个类别的AP再用SAHI跑一遍同样的验证集对比小目标类别的AP变化。如果提升不明显说明瓶颈不在推理阶段得回到训练数据本身去找问题。还有一个验证技巧把验证集按目标像素面积分成小32×32、中32×32到96×96、大96×96三档分别统计每档的AP。如果小目标AP明显低于中和大SAHI值得试如果三档差不多那问题出在别处。这个分档统计用COCO评估工具就能做pycocotools的evaluate函数会直接输出small/medium/large的AP。我自己踩过最深的坑是拿到数据集直接开训训了三天发现mAP只有0.2回头查标注才发现有将近10%的框是越界的还有一批零面积框混在里面。从那以后我养成了一个习惯任何新数据集到手先跑体检脚本再跑一遍可视化随机抽20张图把标注框画出来肉眼过一遍。这20张图花不了十分钟但能省掉后面几十个小时的无效训练。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →