简介:面向道路机器人视觉导航与路面标志识别任务,压缩包内含一套使用YOLOv11标记的完整数据集,适用于自动驾驶、智能车、机器人巡线等场景的算法训练与验证。内容涵盖交通灯、马路、左右转、黄线、人行道、机器人等常见路面导航标志,样本均来自真实道路视频抽帧,画面多样,便于开展目标检测模型的训练、评估与调优。压缩包共815个文件,包含407张jpg图片、407个txt标注文件及1个yaml类别配置文件,整体仅5.52MB,轻量易用;txt文件为标准YOLO格式标注,可直接投入训练,yaml定义了各类别名称与编号,能快速对接YOLOv11工程流程。已有746人学习下载,适合具有初步深度学习基础、需要现成道路标志数据集进行模型验证或毕业设计的开发者快速上手。
1. 道路机器人的路面导航标志识别:为什么这套任务必须用yolov11
道路机器人要在园区、人行道或者厂区路面走,光靠GPS根本不够,真正管用的是地面上这些导航标志:交通灯、左右转箭头、黄线、人行道,还有迎面过来的其他机器人。以前我拿YOLOv5做过一版,能跑,但黄线这种细长目标到了二十米外就断成几截,远处的交通灯干脆就是黑匣子——直接漏检。
换到yolov11之后,同样的数据集,小目标召回率明显上来了,推理速度也没掉多少。下面把整个落地路径捋一遍:数据集怎么采集标注、yolov11怎么训练、怎么在jetson nano上部署,以及那些训练时不踩就白干的血泪坑。适合正在做道路机器人、巡检车或者室外配送车的工程师,目标就一个——让路面导航标志能被稳定识别出来。
2. 路面标志数据集从哪来:采集、标注与四类样本的坑
道路机器人和自动驾驶有一个本质区别:相机装在机器人上,离地只有三五十公分,往前看是路面的斜俯视视角,不是前挡风玻璃那种平视。这个视角差异直接决定了你拿公开数据集来训练,效果大概率翻车。所以第一步不是找模型,是攒一套符合自己视角的数据集。
2.1 用机器人自己的视角采集:相机高度与角度决定样本能不能用
常见做法是:把相机装在机器人前侧,离地30到50厘米,俯角往下压15到30度,让画面里地面占三分之二以上。为什么要压这个角度?因为路面标志比如左右转箭头、黄线,本来就是画在地上的,俯角不够的话它们在画面里会严重透视变形,标注的时候框都拉不准。
采集的时候别只在晴天中午拍。我一般会分四个时段各采一遍:顺光、逆光、阴天、傍晚路灯刚亮。特别是逆光,路面会大面积反光,黄线在画面里会变成一条亮白的线,与白色车道线几乎无法区分。这个情况不采进去,模型上线必出玄学误检。
路面材质也要覆盖。园区常见的是水泥压光地面和沥青路面,黄线在两种材质上的对比度完全不一样。有的厂房地面是绿色环氧地坪,黄色标志线在上面非常显眼,但你如果只把在环氧地坪上训练的模型拿到柏油路上跑,mAP会掉得很直接。这不是模型不行,是样本分布不在一个域里。
采集时建议用ROS或干脆把视频流录成mp4,再按帧抽图,不要一张张按快门。一个实用的抽帧策略是:每2秒抽一帧,去掉连续帧里90%相似的画面,保留标志出现在不同位置、不同距离的样本。这样一批1小时的视频大概能抽到2000到3000张可用的图,足够覆盖标志在视野里从远到近的整个过程。
2.2 标注规范:黄线标成框还是线?人行道标成框还是多边形?
这是这个任务里最容易返工的地方。先说黄线,它是一条细长的线,在画面里宽可能只有10到20个像素,长却占半幅画面。用矩形框标它,框内绝大多数是路面背景,模型学出来的特征会被背景稀释。我踩过这个坑,第一版标了3000个黄线框,训练后预测时黄线框总是比实际范围大一圈,位置还对不齐。
后来我的处理方式是:把一段完整黄线按3到5米切成小段,每段单独标一个矩形框,框贴着线边缘收紧,不要把背景包进来。这样每个框的宽高比虽然很极端,但框内目标占比高,模型反而好学。另外不要标黄线被磨损、断线的地方,那种样本带来的噪声远大于信息量。
人行道是另一个典型。一条完整人行道横跨路面,长宽好几米,用一个框包住会导致目标占比极小。常见做法是按每条白条带单独标注,或者把整条人行道按路面宽度切出两三个框。如果任务只是判断“前方有人行道需要减速”,那切成条带标完全够用;如果要精确规划路径,才考虑用分割方案。
左右转箭头和交通灯要注意类别定义。我的建议是:左右转只标地面上的箭头标志,不把悬空的交通灯信号灯算进来;交通灯单独一个类,只标灯头,红灯、绿灯、黄灯算同一类,因为导航决策只需要知道灯的位置,颜色分类交给后处理。这样两个类别在物理位置上天然分开,模型不容易混淆。
最后是“机器人”这一类。道路机器人面对的其他机器人,外观五花八门,但共性是有轮子、有箱体、高度在一米上下。标注时不要只标正面,侧面、背面都要覆盖,否则模型会只认某个角度的轮廓。我一般要求标注员把画面里所有高度在20厘米以上的移动物体都标出来,宁可多标再删,也别让它在测试集里突然出现时变成黑匣子。
2.3 数据增强与类别平衡:机器人样本里最缺的是“机器人”
整个任务里最缺的样本就是“机器人”这一类。路面标志每天都能采,机器人不是你想见就能见的。最常见做法是:把已有的机器人样本做上下左右翻转、亮度扰动、随机旋转,生成三四倍的量;再不行就把机器人贴到不同路面的背景图上做合成,这招在新场景冷启动时非常管用。
类别平衡上,我一般会统计每个类的框数量,把数量最少的一类设为目标基准。yolov11训练时,如果某类样本过少,可以在data.yaml里给每个类配class_weights,或者简单粗暴地在训练时对少样本类别重复复制。不要指望mosaic增强能完全解决不平衡,它只是把四张图的样本混合到一起,对单类的数量没有实质提升。
数据准备好之后,按8:1:1切训练集、验证集、测试集。注意切分时按“视频片段”切,不要按单帧随机切——同一个视频里相邻帧高度相似,按帧随机切会把相似的帧分到训练和验证里,验证指标虚高,上线后真实效果直接打回原形。
提示:切割数据集前先按“拍摄时间+地点”打个标签,保证同一个场景的视频一定只出现在一个集合里。这是数据泄露最常见的来源。
3. 用yolov11训练导航标志检测模型:环境配置与必调参数
数据集就位,接下来是用yolov11把它训练成一个能用的检测模型。这里涉及环境配置、数据格式转换和训练参数三个环节,任何一个出问题,模型都跑不出理想效果。我从零开始讲一遍我常用的配置方式。
3.1 环境配置:ultralytics与CUDA一起装,别各装各的
yolov11的训练和推理都通过ultralytics这个Python包来跑。最省事的方式是直接用pip安装全家桶,但有个先后顺序讲究:先装PyTorch,再装ultralytics,因为ultralytics依赖torch的版本,反着装容易拿到不匹配的组合。
# 先装与CUDA匹配的PyTorch,这里以CUDA 11.8为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 再装ultralytics pip install ultralytics装完后用一段代码验证环境是否打通:
import torch import ultralytics print("CUDA available:", torch.cuda.is_available()) print("GPU name:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU only") print("Ultralytics version:", ultralytics.__version__)这段代码的作用是确认三件事:torch能不能看到CUDA、GPU是什么型号、ultralytics版本号。如果CUDA available是False,多半是torch装成了CPU版,或者驱动与CUDA版本不匹配。版本号先不用追求最新,只要能和ultralytics配合就行,遇到API报错再针对性升级。
在jetson nano这类ARM设备上,pip装torch会麻烦不少,标准做法是去NVIDIA官网下载JetPack对应版本的torch wheel再pip安装。同一套环境在PC上调试好之后,用requirements.txt锁住版本,到jetson上逐项安装,不要直接拷贝conda环境,ARM和x86的包不通用。
3.2 把标注转成yolov11要的格式:目录结构与data.yaml
标注工具导出的格式五花八门,最常见的是VOC的xml和COCO的json。yolov11训练只认两种输入:每张图对应一个txt,txt里每行是“类别id 中心x 中心y 宽 高”,坐标全部归一化到0到1之间;以及一个data.yaml,描述类别名和数据集路径。写个脚本做转换是常规操作。
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): """ 把VOC格式xml转成yolov11需要的txt xml_path: 标注文件路径 out_dir: 输出目录 class_names: 类别名列表,顺序与id一一对应 """ root = ET.parse(xml_path).getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: continue class_id = class_names.index(name) # VOC的bbox是(xmin, ymin, xmax, ymax)绝对坐标 xmin = float(obj.find("bndbox/xmin").text) ymin = float(obj.find("bndbox/ymin").text) xmax = float(obj.find("bndbox/xmax").text) ymax = float(obj.find("bndbox/ymax").text) # 转成中心点+宽高,并归一化 cx = ((xmin + xmax) / 2) / img_w cy = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines)) # 调用示例 class_names = ["traffic_light", "left_turn", "right_turn", "yellow_line", "crosswalk", "robot"] for xml in os.listdir("annotations"): if xml.endswith(".xml"): voc_to_yolo(os.path.join("annotations", xml), "labels", class_names)这个脚本的核心逻辑是:从xml里读出图片真实尺寸,把绝对坐标的bbox换算成归一化坐标。注意宽和高在归一化时必须用图片的实际宽高,而不是目标检测常用的640x640,因为归一化是相对原图的比例,之后再交给yolov11做letterbox就不会变形。
转完后检查一下txt是否为空和坐标是否越界。我用一个小命令快速扫描:
find labels -name "*.txt" -size 0 -print | head -20空txt说明对应的xml里没有目标,这样的图片放进训练集会变成纯背景样本,少量没问题,多了会干扰模型。坐标越界常见于手工标注时把框拉到了图片边缘外,yolov11训练时会报错,提前用脚本过滤掉比训练到一半再排查省事得多。
3.3 训练命令与必调参数:imgsz、epochs、batch
环境与数据就绪后,训练命令反而简单,ultralytics把训练入口统一封装成了yolo命令。我最常用的是nano模型起步,因为道路机器人场景对算力有约束,先跑通流程再逐步放大模型。
yolo train \ model=yolo11n.pt \ data=/path/to/road_nav.yaml \ epochs=150 \ imgsz=640 \ batch=16 \ device=0 \ workers=4 \ cache=True \ name=road_nav_yolo11ndata.yaml的内容长这样:
path: /path/to/dataset train: images/train val: images/val test: images/test names: 0: traffic_light 1: left_turn 2: right_turn 3: yellow_line 4: crosswalk 5: robot训练参数里有几个直接影响结果,需要重点说。imgsz是训练时缩放到的输入尺寸,路面标志里黄线、远处的交通灯都是小目标,640只是入门,想提升小目标召回率可以试1280,但训练时间与显存消耗会显著上升,且推理时要用同样的imgsz。epochs我一般从150起步,同时开着早停(patience=20),如果验证集mAP连续20轮不涨就自动停,省时间。batch大小受显存限制,16G显存跑yolo11n@640开batch=16比较稳,显存不够就减半,不要强行调workers拉吞吐,数据加载快慢远没有模型收敛重要。device=0表示用第一块GPU训练,多卡时按device=0,1这样写。cache=True会把图片提前缓存进内存,第一次训练会等几分钟,但后面每轮的数据加载速度快一倍以上。对重复迭代调参来说,这个开关非常值。
| 参数 | 我的常用取值 | 说明 |
|---|---|---|
| model | yolo11n.pt | 轻量级版本,后续部署jetson nano不吃力 |
| imgsz | 640 / 1280 | 小目标占比高时直接上1280 |
| epochs | 150 | 配合patience=20做早停 |
| batch | 16 | 16G显存的安全值,不够就减半 |
| device | 0 | 单GPU训练,多GPU按0,1写 |
| cache | True | 缓存图片到内存,加速每轮迭代 |
验证一下训练产物。训练结束后,在runs/detect/road_nav_yolo11n/weights/下会生成best.pt和last.pt,best.pt是验证集mAP最高的权重,last.pt是最后一个epoch的权重。我一般用best.pt做后续部署,但注意,如果训练提前停了而last.pt和best.pt差距很大,说明验证集已经过拟合或训练还没收敛,这时会回溯检查数据集划分。
4. 把模型装进机器人:推理保存与jetson nano部署的最小方案
训练出来的best.pt还只是个权重文件,道路机器人要真正用起来,落地路径分两步:先在PC上做推理验证,再把模型导出成TensorRT引擎装到jetson nano上。这一步做不好,前面训练的所有成果都只能停在demo阶段。
4.1 推理并保存可视化结果:predict命令与results.save()
先跑通一次推理,确认模型在没见过的新图上表现如何。ultralytics的predict会把检测结果直接画在图上,保存成带框的标注图,这个步骤很关键——它不光是看效果,更是在部署前做一次和训练数据同分布的冒烟测试。
from ultralytics import YOLO # 加载训练好的权重 model = YOLO("/path/to/best.pt") # 推理单张图片并保存结果 results = model.predict( source="/path/to/test_image.jpg", conf=0.4, iou=0.5, save=True, save_txt=True, save_conf=True, imgsz=640, project="/path/to/output", name="smoke_test" ) # 遍历结果打印每张图的检测框 for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f"class={model.names[cls_id]}, conf={conf:.3f}, box={xyxy}")这段代码做了三件事:对输入图做letterbox预处理并推理、把结果画到原图并保存、按需导出txt标注和置信度。save_txt=True会输出与YOLO格式相同的txt文件,方便后续做批量评测;save_conf=True则把置信度同时写进txt,统计时直接按这个值画PR曲线。
参数上,conf=0.4是置信度阈值,路面标志里交通灯这类目标置信度通常较高,0.4比较稳;如果你发现漏检多于误检,就把conf往下调到0.25,误检多了再往上调。iou=0.5是NMS的IoU阈值,默认0.5够用,除非同一目标被框了很多次才考虑调到0.7。
批处理也走同一个predict接口,传入一个目录路径即可。我一般会把验证集全图喂一遍,把保存的txt汇总后和标注对比,算每类的mAP。这一步相当于用官方指标检查模型在没见过的数据上到底什么水平,比盯着几张可视化图拍脑袋可靠得多。
4.2 导出TensorRT引擎:jetson nano部署的关键一步
jetson nano的GPU是Maxwell架构,直接跑PyTorch模型浪费得很,必须走TensorRT加速。把yolov11的权重导出成TensorRT引擎,ultralytics已经封装了命令,不用自己写转换脚本。
yolo export model=/path/to/best.pt format=tensorrt device=0 workspace=2导出后在同目录下会生成best.engine文件。这个文件就是TensorRT的可执行引擎,加载方式和加载pt几乎一样,但推理速度会快一个量级。在jetson nano上,yolo11n的engine跑640输入,FP16精度下能到20FPS以上,这对带轮子的道路机器人来说基本够用。
注意几个导出约束。workspace=2表示TensorRT构建时最多用2GB显存做优化搜索,显存小的设备别调太高。导出过程中如果报“TensorRT legacy API”一类的警告,通常是环境里TensorRT版本和ultralytics默认调用的API不匹配,解决方式是检查JetPack版本后安装对应的torch与torchvision,不建议强行忽略警告。
from ultralytics import YOLO # 在jetson nano上直接加载engine推理 engine_model = YOLO("/path/to/best.engine") results = engine_model.predict( source="/dev/video0", conf=0.4, imgsz=640, stream=True ) for result in results: boxes = result.boxes # 这里把检测结果发布给ROS或其他导航节点 print(f"detected {len(boxes)} objects")这段代码有两个关键点:source传的是摄像头设备节点/dev/video0,stream=True表示按视频流模式逐帧返回结果,避免一次性把整个流加载进内存。拿到boxes后建议直接取xyxy坐标、类别id和置信度,发布给下游的避障或路径规划节点,不要在Python层做太多后处理,保持推理循环足够短。
4.3 视频流调用:从摄像头到检测结果的路由姿势
jetson nano上跑视频流,一个常见误区和血泪经验是:用Python在每一帧里先转成numpy数组再做类型转换,导致CPU占用过高。正确做法是让GStreamer或OpenCV直接输出BGR帧,yolov11内部已经做了类型处理,你只负责把帧喂给模型。
import cv2 from ultralytics import YOLO engine_model = YOLO("/path/to/best.engine") cap = cv2.VideoCapture(0, cv2.CAP_V4L2) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while True: ret, frame = cap.read() if not ret: break # imgsz这里要与导出engine时保持一致,否则会多一次resize results = engine_model.predict(frame, imgsz=640, conf=0.4, verbose=False) for r in results: for box in r.boxes: x1, y1, x2, y2 = map(int, box.xyxy[0]) cls_name = engine_model.names[int(box.cls[0])] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f"{cls_name}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("road_nav", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()cap.set里的参数是摄像头输出分辨率,建议设成与模型输入接近的1280x720而不是1920x1080,因为yolov11会把输入压到640去推理,源分辨率再高也是浪费带宽。imgsz=640必须与导出engine时的值一致,不一致时引擎会自动插值,速度会掉,而且精度不可控。
5. yolov11落地避坑:小目标漏检、误检与推理速度的排查记录
模型跑到真机上之后,训练集里的高mAP会变成各种“我明明标了为什么识别不出来”的实战问题。下面是我在这个项目里踩过的四个坑,每条都按现象、原因、解决记录,给后面的人省点时间。
5.1 黄线时断时续:标注没问题但预测连不上
现象:黄线在近距离识别正常,到了10米开外,预测框一段有一段没有,整根线在画面里像虚线。
原因:yolov11的主干网络有多次下采样,黄线这类宽高比极大的目标经过深层特征图后,边缘细节被压缩得厉害;加上我第一版用imgsz=640训练,黄线细部在特征图里只剩一两个像素,漏检就成了必然。
解决:把imgsz提到1280重新训练,黄线的连续性和召回率立刻改善。代价是显存占用翻倍、推理时间从10ms涨到25ms左右,但对道路机器人场景来说完全可接受。另一个有效手段是在标注时按小段标,保证每个框内的黄线占比足够高,模型学到的是“线段的局部特征”而非“一条完整的长线”。
5.2 二十米外的交通灯框不准
现象:近距离的交通灯识别正常,远距离的总是置信度低于0.4被滤掉,或者框的位置偏移半个灯身。
原因:交通灯在640输入下远距离只有十几个像素,属于严格意义上的小目标。yolov11的检测头在特征金字塔的不同层分配目标,默认配置下极小目标容易落到浅层特征上,而浅层特征语义信息不足,分类置信度自然上不去。
解决:除了提高imgsz,我试的最有效的方法是单独收集一批“远距离交通灯”样本,把它们在图像中复制到不同位置,每个位置做小幅缩放和亮度扰动,变相增加小目标样本量。这个方法不改变模型结构,只改变数据分布,收益非常直接。
提示:小目标优化没有银弹。先确认测试集里小目标到底占多少比例、漏检率是多少,再决定是调数据还是调模型结构,不要一上来就魔改网络。
5.3 jetson nano推理只有2-3FPS
现象:把best.pt直接在jetson nano上用Python加载推理,帧率只有2到3FPS,轮子稍微一转画面就卡成PPT。
原因:nano的GPU是128核Maxwell架构,性能有限,PyTorch模型在它上面跑是纯CPU推理加GPU算子混用的状态,算子调度开销远大于计算本身。
解决:导出TensorRT engine后,速度直接拉高8到10倍。如果导完还是慢,检查三个位置:模型是否用了yolo11n而不是更大的s或m;输入imgsz是否从640降到了480或更低;是不是开了太多摄像头分辨率。把这三项压住,20FPS是可复现的数值。
5.4 左右转标志和交通灯互相误检
现象:红色右转箭头被识别成红灯,绿色直行箭头被识别成绿灯。
原因:数据里交通灯样本多、箭头样本少,模型为了最小化损失会倾向于把带“红/绿颜色加圆形/箭头形”的目标都归到样本量大的类别。此外标注时可能有少量框把两个目标同时框进去了,模型学到的是“红色圆形区域=交通灯”这种错误关联。
解决:重新清洗标注,把同时包含箭头和灯头的框拆开;然后对左右转类别做样本补充,再训练。如果还误检,就在后处理加一条规则:交通灯的检测框高度必须大于宽度,箭头框通常宽大于高,用这个先验把明显不合理的预测框过滤掉。
6. 让模型在机器人上跑得更稳:小目标优化与验证步数
到这里模型能跑、能部署,但距离“稳定”还有一段路。最后给两个我会在收尾阶段做的事:针对小目标的调优和上线前的验证方法。
6.1 三个实测有效的小目标优化:提高imgsz、切片推理、P2层
提高imgsz是最没有成本收益比的做法,把训练和推理的imgsz从640提到1280,小目标在特征图里占据的像素量翻倍,mAP通常能涨2到4个点。代价是显存和算力翻倍,jetson nano上考虑帧率就别轻易上。
切片推理的思路是把大图切块分别检测,再把结果合并。对道路机器人这种在路面移动的场景,等于把“远处的小目标”在本地放大后再检测。我用numpy先切成50%重叠的两块,每块推理后再按原坐标拼回。合并时要对重叠区域用NMS去重,否则同一个目标会被框两次。代价是推理总耗时变长,适合机器人停在路口做决策时用,不适合连续行驶中每帧都开。
P2层是yolov11网络结构上一个相对直接的改动:默认检测头从P3开始,P2层对应4倍下采样,特征图分辨率更高,小目标信息保留更完整。Ultralytics的模型定义支持改yaml来打开更浅的检测层,这块改动能进一步提升小目标召回,但显存和耗时增加也明显,我一般只在前面两个方法不够用的时候才做。
6.2 用机器人实测视频验证:统计mAP与漏检率
上线前最后一个动作是录一段10到15分钟的实测视频,覆盖园区里的直行、路口、人行道、会车四个场景,然后用训练好的引擎跑完整段视频,统计每类目标的检测框数量、置信度分布、漏检率。
我先跑一遍直接看,重点关注三类问题:连续帧间同一目标的框是否稳定,抖动严重说明训练样本里该目标角度覆盖不足;黄线在远处是否持续连贯;交通灯在逆光下是否还能保持召回。然后写一个小脚本,把结果csv拉出来按类别算每个类的AR,如果某个类的AR明显低于其他类,就回到数据层面补这个类的样本,而不是调模型参数。
整条路走下来,我最深的体会是:yolov11只是把检测这一环做得更省心,真正决定道路机器人导航识别上限的还是数据和视角匹配。小目标优化别迷信某个trick,先用数据说话,再用结构调优,最后才轮到后处理补救。希望帮到你。
本文还有配套的精品资源,点击获取