YAOTU INSIGHTS

道路机器人视觉导航数据集:VOC标注与YOLO转换实战

道路机器人视觉导航数据集:VOC标注与YOLO转换实战
简介面向道路机器人与自动驾驶视觉感知方向的开发者与学习者这份资源提供了一套以VOC格式标注的路面导航标志识别数据集覆盖交通灯、马路、左右转指示、黄线、人行道及机器人本体等常见道路元素可用于目标检测模型的训练、验证与课程实验。压缩包共816个文件包含407个jpg图像与407个一一对应的xml标注文件另有2个txt说明文件整体约5.75MB图像与标注配对完整便于直接接入YOLO、Faster R-CNN等主流检测框架。目前已有836人学习下载说明该数据集在同类任务中具备一定参考价值。借助成对的图像与VOC标注读者可快速完成数据加载、类别统计、标注校验与模型微调并针对交通灯、车道线、人行道等类别开展识别效果对比与误差分析适合作为机器人导航感知模块的入门练手或算法验证素材。1. 道路机器人视觉导航数据集从交通灯到人行道的 VOC 标注实战做道路机器人导航的兄弟大概率都经历过这个阶段算法框架搭好了ROS 节点跑通了结果一上路发现模型连黄线和人行道都分不清。问题往往不在网络结构而在训练数据——尤其是标注格式不统一、类别定义混乱。我最近拆了一份用 VOC 格式标注的道路场景数据集覆盖交通灯、马路、左右转箭头、黄线、人行道以及机器人本体等路面导航标志图片全部来自实际道路视频抽帧文件名带output_video_mp4前缀说明是连续视频拆出来的序列帧。这份资源适合做机器人视觉导航、自动驾驶感知入门、或者需要快速验证检测模型落地效果的从业者。它解决的核心问题是让你不用从零标注几千张图直接拿到一份类别明确、格式标准的 VOC 数据集把精力放在模型选型和调参上。下面我从数据组织、格式转换、训练接入到踩坑排查一步步拆开讲。2. VOC 格式拆解与道路场景类别体系为什么这样标2.1 VOC 目录结构与标注文件长什么样VOC 格式之所以在检测任务里经久不衰是因为它的目录约定足够简单工具链兼容性极好。一份标准的 VOC 数据集通常长这样VOC2007/ ├── Annotations/ # 每张图对应一个 XML 标注文件 │ ├── output_video_mp4-0668.xml │ └── ... ├── JPEGImages/ # 原始图片 │ ├── output_video_mp4-0668.jpg │ └── ... ├── ImageSets/ │ └── Main/ │ ├── train.txt # 训练集文件名列表不带扩展名 │ ├── val.txt │ └── trainval.txt └── SegmentationClass/ # 分割任务才用检测可忽略每个 XML 文件里记录的是这张图上所有目标框的坐标和类别。拿交通灯举例一个典型的标注节点是这样的object nametraffic_light/name !-- 类别名必须和你的类别表一致 -- poseUnspecified/pose truncated0/truncated !-- 是否被截断0 表示完整 -- difficult0/difficult !-- 是否难以识别训练时可选忽略 -- bndbox xmin234/xmin !-- 左上角 x像素坐标 -- ymin156/ymin !-- 左上角 y -- xmax289/xmax !-- 右下角 x -- ymax312/ymax !-- 右下角 y -- /bndbox /object这里有几个参数直接决定训练质量truncated标记目标是否被画面边缘切断difficult标记是否因为遮挡或过小导致人眼都难判断。很多开源代码默认把difficult1的样本也拿来训练结果模型在密集场景下误检率飙升。我一般会在数据加载阶段加一个过滤开关先统计difficult占比超过 15% 就考虑是否要重新审核这批标注。2.2 道路导航标志的类别定义与边界这份数据集的类别覆盖了机器人上路最常遇到的几类目标交通灯、马路可行驶区域、左右转箭头、黄线、人行道、机器人本体。类别命名看起来简单但实际标注时边界很容易模糊。比如“马路”和“人行道”在画面里往往只隔着一条路沿石标注员如果按语义分割的思路去画框就会把整片区域框进去导致检测模型学到的是一大片背景而不是可行驶区域。常见做法是对“马路”这类大面积类别只标注机器人当前所在车道及相邻车道的可见部分不跨过路沿石对“黄线”这种细长目标框的宽度要略大于线宽避免模型学到过窄的特征导致漏检。左右转箭头通常出现在地面标注时要区分“左转箭头”和“右转箭头”两个独立类别不要合并成“转向箭头”否则模型无法输出方向信息。类别名标注要点常见误标traffic_light只框灯体不框灯杆把整根杆子框进去road只框可行驶路面不跨路沿把人行道一起框入turn_left / turn_right按箭头实际指向分两类合并为一类yellow_line框宽度略大于线宽框太窄导致漏检sidewalk框人行道区域不含路沿石与 road 重叠robot框机器人本体含底盘只框上半身这份数据集的文件名带rf.和哈希后缀说明是经过 Roboflow 类工具导出或增强过的。增强后的图片在训练时能提升泛化但要注意如果增强时做了水平翻转左右转箭头的类别标签必须同步交换否则模型会学到反向映射。我见过有人直接拿翻转后的数据训练结果机器人把左转认成右转血泪经验。2.3 从视频抽帧到 VOC 标注的完整链路如果你手头只有视频想复现这份数据集的制作流程大致链路是视频抽帧 → 筛选有效帧 → 标注 → 导出 VOC。抽帧用 ffmpeg 就行# 每秒抽 2 帧输出为 jpg质量 2数值越小质量越高 ffmpeg -i road_video.mp4 -vf fps2 -q:v 2 frames/%06d.jpg参数fps2表示每秒抽两帧道路场景变化慢2 帧足够覆盖如果是高速场景可以调到 5。-q:v 2控制 JPEG 质量2 到 5 之间比较平衡再低会引入压缩伪影影响标注精度。标注工具推荐 LabelImg 或 CVAT导出时选 Pascal VOC 格式。标注完成后用脚本检查 XML 完整性import os import xml.etree.ElementTree as ET def check_voc_annotations(anno_dir, img_dir): 检查 VOC 标注文件与图片是否一一对应并统计类别分布 missing_img [] class_count {} for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue xml_path os.path.join(anno_dir, xml_file) tree ET.parse(xml_path) root tree.getroot() # 检查对应图片是否存在 img_name os.path.splitext(xml_file)[0] .jpg if not os.path.exists(os.path.join(img_dir, img_name)): missing_img.append(xml_file) # 统计类别 for obj in root.findall(object): name obj.find(name).text class_count[name] class_count.get(name, 0) 1 print(缺失图片的标注文件:, missing_img) print(类别分布:, class_count) check_voc_annotations(VOC2007/Annotations, VOC2007/JPEGImages)这段脚本做两件事一是找出有 XML 但没对应图片的“孤儿标注”这类文件在训练时会直接报错二是统计每个类别的框数量如果某个类别框数不到总数的 5%说明样本严重不均衡需要考虑过采样或加权损失。我一般会在训练前强制跑一遍这个检查避免训练到一半才发现某个类别根本没学进去。3. 把 VOC 转成 YOLO 格式转换脚本与四个边界坑3.1 为什么还要转 YOLO训练框架的格式偏好VOC 是标注格式不是训练格式。虽然有些框架直接吃 VOC但主流检测器如 YOLOv5/v8、MMDetection 更习惯 YOLO 格式或 COCO 格式。YOLO 格式的标注是每张图一个 txt 文件每行一个目标class_id x_center y_center width height坐标全部归一化到 0 到 1 之间。转换本身不复杂但边界情况特别多下面直接给一个我用了很久的转换脚本import os import xml.etree.ElementTree as ET # 类别映射表必须和训练时的 data.yaml 一致 CLASS_MAP { traffic_light: 0, road: 1, turn_left: 2, turn_right: 3, yellow_line: 4, sidewalk: 5, robot: 6 } def voc_to_yolo(xml_dir, img_dir, out_dir): 将 VOC XML 转为 YOLO txt坐标归一化 os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_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): name obj.find(name).text if name not in CLASS_MAP: continue # 跳过未定义类别 difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue # 可选跳过难样本 bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 边界裁剪防止坐标越界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) if xmax xmin or ymax ymin: continue # 跳过无效框 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{CLASS_MAP[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if lines: txt_name os.path.splitext(xml_file)[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) voc_to_yolo(VOC2007/Annotations, VOC2007/JPEGImages, labels)这段脚本里我特意加了三个保护坐标裁剪到图片范围内、跳过无效框宽高为负、跳过difficult1的样本。参数CLASS_MAP必须和训练配置里的类别顺序完全一致否则模型会把交通灯学成马路。归一化用六位小数足够再多没必要。3.2 转换后必须验证的三件事转完不是就完事了我一般会做三步验证。第一步随机抽几张图把 YOLO 框画回去肉眼确认框的位置和类别没错import cv2 def draw_yolo_box(img_path, label_path, class_names): 把 YOLO 标注画回图片用于目视检查 img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: for line in f: cls, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(check.jpg, img) draw_yolo_box(VOC2007/JPEGImages/output_video_mp4-0668.jpg, labels/output_video_mp4-0668.txt, list(CLASS_MAP.keys()))第二步统计每个类别的框数量确认没有类别在转换中丢失。第三步检查归一化坐标是否都在 0 到 1 之间出现大于 1 或小于 0 的值说明原 XML 坐标有问题。3.3 四个边界坑越界、空标注、类别错位、文件名不匹配第一个坑是坐标越界。有些标注员在画框时手抖xmax超过了图片实际宽度转换后归一化坐标大于 1训练时直接报错。上面的脚本已经做了裁剪但裁剪后如果框变得极小建议直接丢弃。第二个坑是空标注文件。一张图里没有任何目标时VOC 的 XML 里没有object节点转换后不会生成 txt 文件。但训练时如果图片列表里有这张图却没有对应 txtYOLO 会当成负样本处理这本身没问题但如果你用的是按 txt 文件遍历的加载器就会漏掉这张图。我一般会显式生成一个空 txt 文件占位。第三个坑是类别错位。CLASS_MAP的顺序和data.yaml里的names列表必须完全一致。我见过有人转换时按字母序排训练时按自定义序排结果模型把robot识别成road机器人直接往人行道上开。第四个坑是文件名不匹配。VOC 的 XML 文件名和图片文件名必须一致扩展名不同但有些工具导出时会加后缀或改大小写。转换前先用脚本比对两个目录的文件名集合差集不为空就先修文件名。4. 训练接入与参数配置从 data.yaml 到推理验证4.1 data.yaml 怎么写路径、类别数、类别名YOLO 格式的数据集需要一个data.yaml来描述路径和类别。针对这份道路机器人数据集我一般这样写path: ./road_robot_dataset # 数据集根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 nc: 7 # 类别数必须和 CLASS_MAP 长度一致 names: # 类别名顺序必须和 CLASS_MAP 一致 - traffic_light - road - turn_left - turn_right - yellow_line - sidewalk - robotnc和names是最容易翻车的地方。nc写错会导致模型输出维度不匹配训练直接崩names顺序写错不会报错但推理结果全乱。我习惯在训练脚本开头加一行断言检查len(names) nc并且把names打印出来和CLASS_MAP的键顺序做比对。4.2 训练参数怎么调小目标与类别不均衡的处理道路场景里交通灯和黄线属于小目标人行道和马路属于大目标尺度差异大。如果用默认的 YOLOv8n小目标召回率通常不理想。我一般会做三个调整把输入尺寸从 640 提到 960增加小目标分辨率开启多尺度训练multi_scaleTrue让模型适应不同距离的目标对黄线和交通灯这类小目标类别在损失里加权重。from ultralytics import YOLO model YOLO(yolov8s.pt) # 用 small 版本平衡速度和精度 model.train( datadata.yaml, epochs120, imgsz960, # 提高输入尺寸利于小目标 batch8, # 显存不够就降到 4 multi_scaleTrue, # 多尺度训练增强尺度泛化 lr00.01, # 初始学习率 lrf0.01, # 最终学习率系数 warmup_epochs3, # 预热轮数防止早期震荡 cos_lrTrue, # 余弦退火后期收敛更稳 patience30, # 30 轮无提升就早停 device0 # GPU 编号CPU 写 cpu )imgsz960是这份数据集的关键参数640 下交通灯可能只有十几个像素模型根本学不到。batch8在 8G 显存下比较稳再大容易 OOM。patience30是后悔药防止过拟合后还在跑。4.3 推理验证用视频流测一遍才知道行不行训练完在验证集上 mAP 高不代表路上能用。我一般会拿一段实际道路视频做推理看连续帧里的检测稳定性import cv2 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) cap cv2.VideoCapture(road_test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break results model(frame, conf0.4, iou0.5) # conf 阈值 0.4iou 0.5 annotated results[0].plot() cv2.imshow(result, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()conf0.4是我在道路场景常用的阈值再低误检多再高漏检多。iou0.5控制 NMS 合并黄线这种细长目标如果挨得近iou 可以调到 0.3 避免被合并掉。看视频推理结果时重点看三件事交通灯在远距离是否稳定检出、左右转箭头有没有被混淆、人行道和马路边界是否清晰。如果连续帧里框在跳动说明模型对纹理敏感需要加时序平滑或增加数据增强。5. 避坑与常见问题排查标注、转换、训练里的真实翻车记录5.1 现象训练 loss 正常下降但 mAP 始终为 0原因通常是类别名和标注文件里的name不匹配。VOC XML 里写的是traffic_light但data.yaml里写的是traffic light空格代替下划线加载器找不到对应类别所有框被忽略。解决方法是写一个脚本遍历所有 XML收集name字段的唯一值和data.yaml的names做集合比对差集不为空就先修标注或改配置。5.2 现象推理时左右转箭头频繁互换原因大概率是数据增强时做了水平翻转但没交换左右标签。YOLO 默认开启fliplr0.5如果数据集里左转和右转样本数量接近翻转后左转变右转标签却没变模型就学反了。解决方法是在data.yaml同级加一个自定义增强配置把fliplr设为 0或者写一个回调在翻转时同步交换类别 ID。我一般对方向敏感的数据集直接关掉水平翻转用旋转和色彩抖动替代。5.3 现象黄线检测框断断续续视频里闪烁原因是黄线属于细长小目标默认锚框尺寸偏大模型学不到细长特征。解决方法有两个一是用 K-means 对训练集框做聚类重新生成锚框二是把输入尺寸提到 1280让黄线在特征图上有更多像素。我通常先试提尺寸不行再改锚框。另外 NMS 的 iou 阈值调到 0.3 到 0.4避免相邻黄线段被合并。5.4 现象转换后的 txt 文件为空但原 XML 有框原因是 XML 里bndbox的坐标是浮点数且带单位或者xmin大于xmax。有些标注工具导出时坐标顺序会反转换脚本里xmax xmin的判断会直接跳过。解决方法是转换前先跑一遍 XML 校验把坐标异常的标注文件列出来人工复核。我一般会在转换脚本里加日志记录每个被跳过的框和原因方便回溯。5.5 现象训练到一半报显存不足原因是batch或imgsz设太大或者workers开太多导致数据加载占用显存。解决方法是先把batch减半如果还不行就降imgsz最后再调workers。另外 YOLO 的cacheTrue会把图片缓存到内存数据集大时反而拖慢建议关掉。我一般会在训练前用nvidia-smi看一下显存占用留 1G 余量给系统。6. 进阶技巧用切片推理提升小目标召回与标注复核效率小目标检测有个很实用的技巧叫切片推理SAHI把大图切成小块分别推理再合并交通灯和黄线这种小目标召回率能提升一截。用法不复杂from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model AutoDetectionModel.from_pretrained( model_typeyolov8, model_pathruns/detect/train/weights/best.pt, confidence_threshold0.4, devicecuda:0 ) result get_sliced_prediction( road_frame.jpg, detection_model, slice_height512, # 切片高度 slice_width512, # 切片宽度 overlap_height_ratio0.2, # 垂直重叠比例 overlap_width_ratio0.2 # 水平重叠比例 ) result.export_visuals(export_dirsahi_output/)slice_height和slice_width设成 512 是因为交通灯在原图里可能只占 30 到 50 像素切完后在切片里占比更大模型更容易学到。overlap设 0.2 是为了避免目标正好落在切片边缘被切断。这个技巧的代价是推理速度变慢实时性要求高的场景要权衡。另一个我常用的习惯是每次标注完新数据先用当前模型跑一遍推理把模型置信度低但人工确认有目标的帧挑出来优先复核这些帧。这样标注效率能提升不少模型迭代也更快。从那以后我每次拿到新数据集都强制走一遍“转换校验 → 画框目视 → 视频推理”三步宁可前期多花半小时也不想训练到一半才发现标注有问题。希望帮到你。本文还有配套的精品资源点击获取