简介这份资源面向道路机器人视觉导航方向的开发者与学习者聚焦交通灯、马路、左右转、黄线、人行道及机器人等路面标志的识别任务采用VOC格式标注可直接用于目标检测模型的训练与验证。压缩包共816个文件包含407个jpg图像与407个对应的xml标注文件另有2个txt说明文件整体约5.75MB图像与标注一一对应便于快速构建数据集并接入YOLO、Faster R-CNN等主流框架。内容覆盖多类道路场景与导航标志适合作为课程设计、竞赛项目或科研实验的数据基础。目前已有836人学习下载读者可借此省去自行采集与标注的成本直接开展模型训练、类别统计与效果评估也能通过标注文件理解VOC格式的字段组织方式为后续数据增强与迁移学习提供便利。1. 道路机器人识别交通灯、马路与导航标志VOC 格式到底解决了什么问题一台在园区里跑的道路机器人最怕的不是算力不够而是把「能走」和「不能走」看反了。红灯当成绿灯冲出去、把黄线压成可通行区域、人行道和机动车道分不清这些都不是模型精度小数点后两位的问题而是直接决定这台机器人能不能上路。标题里这一串目标——交通灯、马路、左右转、黄线、人行道、机器人等路面导航标志——本质上是一套面向移动机器人的视觉感知清单而 VOC 格式则是把这套清单变成可训练数据的通用容器。VOC 格式PASCAL VOC在工业界被反复使用不是因为它先进而是因为它足够简单一张图对应一个 XML框的位置、类别名、遮挡和截断状态都写在里面任何检测框架都能吃。对做机器人导航的团队来说选 VOC 往往意味着两件事一是标注工具链成熟LabelImg 这类工具直接产出二是历史数据集和自采数据能混着用不用为了一个新类别重做整套流水线。适合谁适合手上已经有摄像头、有 ROS 或类似中间件、准备把感知模块从「能出框」推进到「能指挥决策」的团队。下面按数据、模型、部署、避坑、进阶五段讲透。2. 把路面导航标志做成 VOC 数据集类别设计与标注边界2.1 类别怎么定别把「左转」和「左转待转」混成一个类路面导航标志识别最容易翻车的地方不在模型而在类别表。标题里列了交通灯、马路、左右转、黄线、人行道、机器人等如果直接照抄成 6 个类训练出来的模型会在路口疯狂误检。我的做法是先按「决策用途」拆交通灯要拆成 red / green / yellow / off 四类因为机器人要的是灯色而不是灯箱左右转要拆成 left_arrow / right_arrow / straight_arrow因为箭头方向直接对应转向指令黄线单独成类因为压线是违规判定人行道和马路可以合并成 drivable / non_drivable 两个语义类但框的粒度要统一。一个可用的类别表示例classes.txt每行一个类名顺序即 label idtraffic_light_red traffic_light_green traffic_light_yellow traffic_light_off arrow_left arrow_right arrow_straight yellow_line crosswalk drivable_area non_drivable_area robot逻辑说明前四类覆盖灯色状态中间三类覆盖转向箭头yellow_line 和 crosswalk 是规则类drivable / non_drivable 是语义类robot 用于多机场景下的相互避让。参数上类别顺序一旦确定就不要改因为 VOC XML 里存的是名字但转 YOLO 时用的是索引改顺序等于让所有旧标签错位。2.2 标注边界黄线和人行道最容易标出「玄学框」黄线是细长目标标注时如果贴着线画一个 2 像素宽的框模型学不到如果画太宽又会把路面吞进去。我的经验是黄线框的宽度至少覆盖线宽加两侧各 3 像素长度按可见段连续标注被车压断的地方断开成两个框。人行道按斑马线整体外接矩形标不要逐条白线标否则类别内方差过大。交通灯框要包含灯箱外壳不要只框亮着的灯珠因为 off 状态也需要样本。标注完成后每个 XML 至少检查三件事size 里的 width/height 和实际图片一致、每个 object 的 name 在 classes.txt 里、bndbox 的 xmin xmax 且 ymin ymax。这三条不过后面训练 loss 会莫名其妙不降。2.3 用脚本把 LabelImg 产出转成训练清单LabelImg 默认存 VOC XML但训练框架通常要一个 txt 清单。下面这个脚本把 JPEGImages 和 Annotations 对齐生成 train/val 列表并顺手过滤掉空标注和越界框import os import xml.etree.ElementTree as ET import random IMG_DIR JPEGImages ANN_DIR Annotations OUT_DIR ImageSets/Main os.makedirs(OUT_DIR, exist_okTrue) def parse_xml(path): tree ET.parse(path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) boxes [] for obj in root.findall(object): name obj.find(name).text bb obj.find(bndbox) xmin int(float(bb.find(xmin).text)) ymin int(float(bb.find(ymin).text)) xmax int(float(bb.find(xmax).text)) ymax int(float(bb.find(ymax).text)) # 过滤越界和零面积框 if xmin 0 or ymin 0 or xmax w or ymax h or xmax xmin or ymax ymin: continue boxes.append((name, xmin, ymin, xmax, ymax)) return boxes ids [os.path.splitext(f)[0] for f in os.listdir(ANN_DIR) if f.endswith(.xml)] valid [] for i in ids: img_path os.path.join(IMG_DIR, i .jpg) if not os.path.exists(img_path): continue if len(parse_xml(os.path.join(ANN_DIR, i .xml))) 0: continue valid.append(i) random.seed(42) random.shuffle(valid) split int(len(valid) * 0.8) with open(os.path.join(OUT_DIR, train.txt), w) as f: f.write(\n.join(valid[:split])) with open(os.path.join(OUT_DIR, val.txt), w) as f: f.write(\n.join(valid[split:])) print(train:, split, val:, len(valid) - split)逻辑说明先解析 XML 拿到宽高和框过滤掉越界框和零面积框再按 8:2 切分。参数上 random.seed(42) 保证可复现如果数据里有连续视频帧不要随机切要按视频段切否则相邻帧同时进 train 和 val 会让指标虚高。这一步做完VOC 数据集才算真正可用。3. 训练一个能认灯、认线、认人行道的检测模型3.1 选型为什么路面导航标志更适合单阶段检测器路面导航标志识别对实时性要求高机器人本体算力通常有限常见做法是选 YOLO 系列或 SSD 这类单阶段检测器。两阶段检测器精度可能略高但推理延迟在嵌入式平台上很难压到 30ms 以内。我的建议是如果机器人主控是 Jetson 或同级算力直接上 YOLOv5/v8 的 s 或 n 版本如果是更低算力考虑 MobileNet-SSD。选型时不要只看 mAP要看小目标召回——交通灯和箭头在远距离下只有几十像素这才是真正决定机器人能不能提前减速的指标。3.2 从 VOC 转 YOLO 格式转换脚本与四个边界坑YOLO 训练要的是每张图一个 txt每行class_id cx cy w h全部归一化到 0~1。下面脚本把 VOC XML 转成 YOLO txtimport os import xml.etree.ElementTree as ET CLASSES [traffic_light_red, traffic_light_green, traffic_light_yellow, traffic_light_off, arrow_left, arrow_right, arrow_straight, yellow_line, crosswalk, drivable_area, non_drivable_area, robot] cls2id {c: i for i, c in enumerate(CLASSES)} ANN_DIR Annotations OUT_DIR labels os.makedirs(OUT_DIR, exist_okTrue) for f in os.listdir(ANN_DIR): if not f.endswith(.xml): continue root ET.parse(os.path.join(ANN_DIR, f)).getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in cls2id: continue bb obj.find(bndbox) xmin float(bb.find(xmin).text) ymin float(bb.find(ymin).text) xmax float(bb.find(xmax).text) ymax float(bb.find(ymax).text) # 裁剪到图像范围内避免归一化后出现负值 xmin max(0, min(xmin, w)) xmax max(0, min(xmax, w)) ymin max(0, min(ymin, h)) ymax max(0, min(ymax, h)) if xmax xmin or ymax ymin: continue cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls2id[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(OUT_DIR, f.replace(.xml, .txt)), w) as out: out.write(\n.join(lines))逻辑说明四个边界坑分别是——类别名不在表里要跳过、坐标要裁剪到图像内、零面积框要丢、归一化用图像真实宽高而不是 XML 里可能写错的 size。参数上保留 6 位小数足够YOLO 读取时会再解析。转完后随机抽 20 张用可视化脚本画框核对这一步不能省。3.3 训练参数小目标召回靠 imgsz 和 anchor以 YOLOv5 为例路面导航标志训练的关键参数不是 epochs而是 imgsz 和 anchor。imgsz 建议 640 起步如果交通灯在图中占比小于 1%提到 960 或 1280但显存要跟上。anchor 用默认的 COCO anchor 往往偏大可以用--noautoanchor关掉自动锚点再跑一次 k-means 聚类自己的框。学习率用 0.01 配 SGDwarmup 3 个 epoch数据增强里 mosaic 开到 1.0 但 close_mosaic 设 10让最后 10 个 epoch 用真实分布收尾。python train.py --data road_robot.yaml --img 960 --batch 16 --epochs 120 \ --weights yolov5s.pt --cfg yolov5s.yaml --noautoanchor \ --hyp data/hyp.scratch-low.yaml --name road_robot_v1逻辑说明--img 960提升小目标分辨率--noautoanchor避免默认锚点和细长黄线不匹配--hyp hyp.scratch-low.yaml降低初始学习率防止发散。训练时盯三个指标交通灯各类的 recall、黄线的 mAP0.5、以及验证集里 drivable_area 和 non_drivable_area 的混淆情况。如果黄线 mAP 长期低于 0.3先回去查标注宽度而不是加数据。4. 部署到机器人从检测框到导航指令的映射4.1 后处理把框变成「停、走、左、右」模型输出的是框机器人要的是指令。中间这层后处理常被忽略却是最容易出事故的地方。我的做法是写一个状态机交通灯取画面下半部最大的红/绿框连续 3 帧同类才确认箭头取最近的前方框按类别映射转向黄线如果框与机器人规划路径重叠超过阈值触发压线告警人行道检测到 crosswalk 且有人形框时降速。下面是一个简化的决策片段def decide(dets, frame_w, frame_h): # dets: list of (cls_name, conf, xmin, ymin, xmax, ymax) cmd {speed: 1.0, steer: 0.0, warn: None} lights [d for d in dets if d[0].startswith(traffic_light) and d[1] 0.5] if lights: # 取画面下半部、面积最大的灯 lights.sort(keylambda d: (d[5] - d[3]) * (d[4] - d[2]), reverseTrue) name lights[0][0] if name traffic_light_red: cmd[speed] 0.0 elif name traffic_light_yellow: cmd[speed] 0.3 arrows [d for d in dets if d[0].startswith(arrow) and d[1] 0.5] if arrows: arrows.sort(keylambda d: d[5], reverseTrue) # 取最靠下的 if arrows[0][0] arrow_left: cmd[steer] -0.5 elif arrows[0][0] arrow_right: cmd[steer] 0.5 lines [d for d in dets if d[0] yellow_line and d[1] 0.4] if lines: cmd[warn] yellow_line return cmd逻辑说明灯色按面积排序取最大避免远处小灯干扰箭头按 ymax 取最靠下的因为近处箭头才是当前路口黄线只告警不直接停车留给规划层处理。参数上 conf 阈值灯用 0.5、黄线用 0.4是因为黄线细长容易低分阈值太高会漏。这套后处理要单独写单元测试用录制的 rosbag 回放验证不要直接上车调。4.2 推理加速TensorRT 和半精度怎么选机器人端常见做法是把 PyTorch 模型导出 ONNX 再转 TensorRT。FP16 在 Jetson 上通常能带来 1.5~2 倍加速精度掉不到 1 个点INT8 更快但需要校准集路面标志类别少、颜色敏感INT8 容易把黄线和路面混掉我一般不用。导出时注意 opset 选 11 或 12动态 batch 如果不需要就固定为 1减少显存碎片。python export.py --weights road_robot_v1.pt --include onnx --opset 12 --img 960 trtexec --onnxroad_robot_v1.onnx --saveEngineroad_robot_v1.fp16.engine --fp16逻辑说明--opset 12兼容性好trtexec --fp16生成半精度引擎。部署后要用同一批验证图对比 PyTorch 和 TensorRT 的输出框IoU 低于 0.9 的样本要查通常是预处理归一化方式不一致导致的。5. 避坑与排查路面导航标志识别最常见的 5 个翻车现场5.1 现象训练 loss 正常下降但交通灯全检成 off原因数据里 off 状态样本过多且红绿灯亮灯样本的灯珠区域太小模型学到的是灯箱形状而不是颜色。解决对红绿黄样本做过采样或在损失里给亮灯类更高权重同时检查标注是否只框了灯箱没框灯珠必要时把灯珠区域也纳入框内。5.2 现象黄线 mAP 始终低于 0.3可视化发现框飘原因黄线标注宽度不一致有的 2 像素有的 20 像素模型无法收敛到统一尺度。解决统一标注规范框宽 线宽 6 像素如果线被遮挡断开标注而不是拉长框训练时对黄线类单独统计正样本数少于 500 个就补数据。5.3 现象验证集指标很好上车后路口频繁误刹原因验证集和训练集来自同一段视频相邻帧泄漏指标虚高。解决按视频段切分 train/val确保同一段路的帧只出现在一边上车前用另一天、另一路段的数据做一次盲测mAP 掉超过 10 个点就说明泛化不够。5.4 现象TensorRT 推理结果和 PyTorch 对不上框整体偏移原因导出 ONNX 时预处理用了 letterbox但 TensorRT 侧没做同样的 padding导致坐标映射错位。解决把 letterbox 的参数缩放比、padding一起写进推理脚本或者在导出时固定输入尺寸并关闭 letterbox两边用同一套预处理。5.5 现象机器人把「直行箭头」识别成「左转箭头」转向错误原因箭头类样本中直行和左转的视觉差异小且标注时箭头方向被旋转或镜像增强破坏。解决检查数据增强里有没有随机水平翻转箭头类不能翻转对直行和左转做难例挖掘把误检样本加进训练集后处理里加连续帧投票单帧误检不触发转向。6. 进阶技巧用半自动标注和类别平衡把迭代周期压到一天数据迭代速度决定这个方向值不值得长期投入。我的习惯是先用一版模型对未标注视频做推理把置信度高于 0.7 的框转成 VOC XML 预标注人工只改错的标注效率能提 3 倍以上。下面脚本把推理结果写成 VOC XML直接进 LabelImg 复核import os import xml.etree.ElementTree as ET from xml.dom import minidom def write_voc(img_name, w, h, dets, out_dir): ann ET.Element(annotation) ET.SubElement(ann, folder).text JPEGImages ET.SubElement(ann, filename).text img_name size ET.SubElement(ann, size) ET.SubElement(size, width).text str(w) ET.SubElement(size, height).text str(h) ET.SubElement(size, depth).text 3 for name, conf, xmin, ymin, xmax, ymax in dets: if conf 0.7: continue obj ET.SubElement(ann, object) ET.SubElement(obj, name).text name ET.SubElement(obj, difficult).text 0 bb ET.SubElement(obj, bndbox) ET.SubElement(bb, xmin).text str(int(xmin)) ET.SubElement(bb, ymin).text str(int(ymin)) ET.SubElement(bb, xmax).text str(int(xmax)) ET.SubElement(bb, ymax).text str(int(ymax)) xml_str minidom.parseString(ET.tostring(ann)).toprettyxml(indent ) with open(os.path.join(out_dir, img_name.replace(.jpg, .xml)), w) as f: f.write(xml_str)逻辑说明置信度阈值 0.7 是经验值太低会引入大量误检增加复核负担太高会漏掉难例。预标注只用于加速不能直接当 ground truth人工必须过一遍。类别平衡方面我一般每轮统计各类框数量对少于总数 5% 的类做复制增强或专门补采交通灯的 off 类如果太多就下采样避免模型偏向。最后说个我自己的教训早期做这个方向时我花了两周调模型结构mAP 只涨了 1 个点后来把黄线标注规范统一、把验证集按路段重切同样的模型 mAP 涨了 8 个点。路面导航标志识别这件事数据质量的天花板远高于模型结构先把 VOC 标注和类别表做扎实再谈调参和加速。希望帮到你。本文还有配套的精品资源点击获取