简介:面向道路机器人视觉识别与自动驾驶导航场景,这份资源提供了基于真实道路视频帧构建的YOLOv11标注数据集,覆盖交通灯、马路、左右转、黄线、人行道、机器人等常见路面导航标志,适合计算机视觉学习者、机器人开发者以及自动驾驶项目人员用于目标检测模型的训练与验证。压缩包共815个文件,包含407张jpg图像与407个对应的txt标签文件,另外配有1个yaml配置文件,可直接用于YOLO系列模型训练;整体仅5.52MB,轻量易取,便于快速开展实验。目前已有746人学习下载,数据源自不同道路场景的视频抽帧,标注类别丰富,图像与标签一一对应,yaml文件已内置类别名称,导入YOLOv11即可开始训练,省去繁琐的数据整理环节。该数据集既可用于算法效果评估,也可作为课程设计或毕业设计的实践素材,帮助读者掌握从数据准备到模型部署的完整流程。
1. 道路导航标志识别:为什么最难的是一根黄线,而不是交通灯
把 yolov11 用在道路机器人上,最常见的诉求就是识别交通灯、左右转箭头、黄线、人行道,以及视野里出现的其他机器人——这个目标检测任务做出来后,机器人才能知道该直行、转弯还是停车。很多第一次接触这个项目的工程师会下意识把所有注意力放在交通灯上,觉得那是最小的目标、最难的类别。但实际跑下来你会发现,真正让模型频繁翻车的是那根连续几百米的黄线:它又细又长,大部分情况下占整个画面的比例不到 2%,标注时一不注意就标成一条横跨整张图的巨型框,训练时不是漏检就是误检。这篇笔记就是围绕 yolov11 把这套路面标志识别做扎实的过程,从数据标注、训练调参到 Jetson 上部署,把每个环节的取舍和踩坑讲清楚,适合正在做巡检机器人、园区物流车或者低速自动驾驶感知的工程师。
2. yolov11 处理路面标志的选型理由与数据改造
2.1 路面标志检测里 yolov11 的优势:小目标、长条目标与端侧部署
道路机器人的感知环境很不一样:摄像头高度低,大概在 0.5 到 1.2 米左右,视野里既有平视的交通灯、也有俯视的箭头和人行道。这种多视角混合场景对检测模型有两个硬性要求——小目标不能丢,长条目标不能断。yolov11 在 yolov8 的基础上把 C2f 模块换成了 C3k2,同时保留了 anchor-free 的解耦头策略,这意味着它对细长目标的响应更均匀。我对比过同样的数据在 yolov8s 和 yolov11s 上的表现,长条黄线这类极端长宽比的目标,v11 的召回率普遍能高 3 到 5 个点,主要原因是它的动态标签分配对形状不那么敏感,不会因为目标跨度过大就只给一个格子分配正样本。
另一个选型理由是端侧部署的便利性。道路机器人一般不会扛着 4090 出门,常见的是 Jetson Orin Nano 或者 Jetson Nano,甚至是树莓派加算力棒。yolov11 的模型系列里 n、s、m 三个档位都提供了很好的性能梯度,而且 ultralytics 这个仓库把训练、导出、TensorRT 转换的接口统一了,省去自己改装模型结构的功夫。相比分割模型、或者基于 transformer 的检测器,yolov11 在端侧是更容易落地的方案,内存占用可控,推理延迟能压到 30ms 以内。博主做导航标志识别项目时几乎不会在这类任务上首选大模型,因为路面标志类别本身并不复杂,通常只有六七个类,m 已经足够,再大的模型只是增加部署成本,精度收益非常有限。
2.2 把道路机器人的视频帧转成 YOLO 标注格式:归一化坐标与类别映射
路面标志识别项目在数据准备阶段,最常见的问题不是图片不够,而是标注格式不统一。很多团队从不同渠道拿到数据:有的是自动驾驶数据集里抽出来的帧,有的是标注公司给的 JSON 格式,有的是之前用老版本工具标出来的 VOC XML。落到 yolov11,最终需要的是一片 root 目录下按 train、val 分开的图片文件夹,和每张图片同名同前缀的 txt 文件。txt 每行是一个目标,格式为 class x_center y_center width height,全部是相对图片宽高的归一化数值。下面这个脚本是我常用的转换工具,能把常见的归一化框或者像素坐标统一转到 YOLO 格式。
import os import json import cv2 def convert_to_yolo(json_path, output_dir, class_map): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) os.makedirs(output_dir, exist_ok=True) for image_info in data['images']: img_path = image_info['path'] img = cv2.imread(img_path) h, w = img.shape[:2] # image_info['annotations'] 内含 bbox,格式为 [x1, y1, x2, y2],像素坐标 yolo_lines = [] for ann in image_info['annotations']: cls_name = ann['category'] # 例如 'yellow_line' if cls_name not in class_map: continue cls_id = class_map[cls_name] x1, y1, x2, y2 = ann['bbox'] # 转成 xywh 并用图片宽度和高度归一化 box_w = x2 - x1 box_h = y2 - y1 x_center = (x1 + x2) / 2.0 / w y_center = (y1 + y2) / 2.0 / h box_w_norm = box_w / w box_h_norm = box_h / h yolo_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w_norm:.6f} {box_h_norm:.6f}") base_name = os.path.splitext(os.path.basename(img_path))[0] txt_path = os.path.join(output_dir, base_name + '.txt') with open(txt_path, 'w') as fp: fp.write('\n'.join(yolo_lines)) convert_to_yolo( 'road_robot_annotations.json', 'yolo_annotations', class_map={ 'traffic_light': 0, 'left_turn': 1, 'right_turn': 2, 'yellow_line': 3, 'crosswalk': 4, 'robot': 5, 'road': 6 } )这个脚本的核心是把 JSON 里任意格式的像素坐标转成 YOLO 的归一化中心点加宽高。需要注意一个细节:如果原始标注是 [x1, y1, x2, y2],宽高直接相减就能得到;但不少标注工具给的是 [x_center, y_center, w, h],两种格式混在一起时,一定在循环里打印前 5 行核对。转换完成后,检查每张图的 txt 是否为空——空的就说明原标注漏了或者类别名没匹配上,在训练前要单独抽出来复核。
和分类映射表配套的data.yaml也放在同一层级,内容的写法如下。注意 nc 一定和 class_map 的长度一致,否则 ultralytics 训练时会静默报错或者把类别顺序错掉,这是最容易忽略的一个坑。
path: ./road_robot_dataset train: images/train val: images/val names: 0: traffic_light 1: left_turn 2: right_turn 3: yellow_line 4: crosswalk 5: robot 6: road2.3 长条目标的标注策略:黄线、人行道与马路边界怎么切
路面标志识别项目里的数据标注,新人最容易犯的错误是把整条黄线从头到尾框成一个巨大的矩形。比如一张 1920x1080 的图里黄线从左边 100 像素延伸到右边 1700 像素,高只有 30 像素,如果标成一个框,这个框的宽高比接近 53:1。检测头对这种极端长宽比的框非常不适应,因为 anchor-free 的标签分配是基于框中心点距离的,一个横跨全图的框会让几乎所有网格都觉得“这是自己的目标”,反而导致训练不稳定。
我一般处理长条目标的思路是沿长轴切分,每段控制在 100 到 250 像素长度,段与段之间留 10 到 20 像素的重叠。本质上是用多个正常长宽比的框去逼近一条线,推理时再用相邻框的中心点连线做后处理,生成平滑的导航参考线。这个做法对黄线尤其关键,因为黄线是机器人变道和对向避让的边界依据,断断续续的检测结果没法直接用。
人行道斑马线也适用这个思路。每条斑马线是一段窄而长的白色条纹,但它是离散的,所以更合理的标注是每一条白条纹单独一个框,而不是把整片人行道圈起来。左右转箭头则要注意,它本身是一个带方向的图形,标注框的边界应该紧贴箭头图形的实际可见区域,不要为了省事把整条车道都包进去,否则模型学到的是“这段路面是左转箭头”,换个路口没有画箭头也会误检。
还要说的是“马路”这一类的定义。道路现场的“马路”实际上不是一个标准的检测目标,很多数据集会把它标成道路区域、马路边缘或者可见路面。如果是做简单的路权判断,可以当成一个目标,但尽量标注为车道边界线或路沿,而不是把整个天空以下都标成马路。标注原则是“导航能用、边界干净”,不必追求像素级准确。
3. 用 yolov11 训练路面标志识别:小目标优化与损失权重
3.1 模型结构选择:yolov11s 和 yolov11m 在端侧工况下的取舍
训练路面标志识别模型,第一步是选模型尺寸。在 Jetson Orin Nano 8G 上做部署的话,yolov11s 是性价比最高的选择,单帧 640 分辨率下 TensorRT FP16 推理延迟大概在 15 到 22ms;如果换 yolov11m,延迟会涨到 30 到 45ms,但精度提升主要集中在小目标上,约 2 到 4 个点。具体取舍要看机器人的决策频率:如果你做的是车速 5km/h 以下的巡检机器人,30ms 到 50ms 完全可接受,直接上 m;如果车速到了 15km/h 以上,一个 30ms 的延迟意味着 12 厘米以上的位置误差,这时候我建议用 s 而不是 m,再用输入分辨率补小目标精度。
yolov11n 不建议用在路面标志识别上,因为 n 的参数量砍得太狠,对黄线和交通灯这类低纹理目标的特征提取明显不够。如果目标是 Nano 2G 这种极低内存设备,宁可用 s 模型配合动态分辨率,比如交通灯检测 1280 分辨率,其他类别 640 分辨率。两个模型并行开销不小,但比强行压缩成 n 要靠谱。
3.2 训练配置:输入分辨率、关闭 Mosaic 与类别损失权重
路面标志识别训练阶段有几个和通用目标检测不太一样的配置。第一,输入分辨率我一般从 640 起步,但交通灯占比大于 3% 的高清数据建议直接用 1280 训练,因为 yolov11 的检测头在 640 分辨率下对 20x20 像素的小交通灯基本无能为力。1280 会导致训练显存占用上升,yolov11s 大概要多占 4G 显存,如果没有大显存卡,可以使用 imgsz=960 作为折中。
第二,非常关键的是关闭 Mosaic 增强。Ultralytics 默认在训练前 10 个 epoch 启用 Mosaic,把四张图拼成一张,这能让模型学到更丰富的背景。但路面标志是强上下文依赖的目标:黄线需要依靠周围路面颜色来分辨,交通灯需要依靠灯架和背景天空来定位。Mosaic 把四张不同场景拼在一起,会切断这些上下文,典型的表现是小目标漏检率升高。所以这个项目里我会直接设置:
mosaic: 0.0 # 关闭四图拼接 mixup: 0.2 # 保留轻微 mixup,帮助类别区分 hsv_h: 0.01 # 色调变化调小,避免黄色线颜色偏移 hsv_s: 0.2 hsv_v: 0.2 fliplr: 0.5 degrees: 2.0 # 路面标志角度变化很小,旋转幅度不要给大 translate: 0.1 perspective: 0.0001上面这些参数是写在 ultralytics 默认的 hyp 配置文件里的。其中hsv_h特别值得注意,色调变化如果保持默认的 0.015,会让黄线在数据增强过程中变成橙线甚至红棕线,训练后模型对“黄色”的约束会放松,误检率明显上升。我把hsv_h压到 0.01 以下,本质上是告诉模型“黄线必须是黄的”。
第 3 节提到的类别不均衡问题,用损失权重来改善。路面标志数据集的类别数量天然不均衡:黄线可能是 road 类的 10 倍以上,交通灯可能只有几十张图。Ultralytics 支持在训练时通过命令行附加权重,但更直接的做法是控制采样。我会用class_weight参数:
yolo train data=road_dataset.yaml model=yolov11s.pt imgsz=1280 epochs=150 batch=16 mosaic=0.0 box=7.5 cls=1.0 dfl=1.5 device=0这个命令里,box=7.5是提高边界框回归的权重,对长条目标有效果;cls=1.0保持默认,避免因为权重过大导致类别过拟合;dfl=1.5增大了分布聚焦损失,有利于箭头这种边缘锐利的目标。不建议盲目把某个类别权重拉高到 5 以上,实测超过 3 后会出现同一张图里对同一目标重复输出的问题,后期 NMS 都压不住。
3.3 让长条目标更容易被召回:切分以外的替代方案
切分标注不是唯一思路。有人在长条目标上试过把检测框旋转,但 yolov11 的检测头是轴对齐的,旋转框只能靠后处理还原。我试过两种替代方案:一是把黄线段落分成上、下两条分别标注,靠模型自己去学“两条线之间的区域是黄线范围”;二是增加一个“黄色区域”类的语义分割辅助头,不过这会引入额外的训练复杂度。
在纯 yolov11 框架内求稳,切分加固还是最可靠的。推理时把同一帧里所有检测到的黄线框按中心点 y 坐标聚类,再按从左到右连接,就能生成一条接近真实的黄线轨迹。这个后处理逻辑在 4.3 节里会给出脚本。
另外补充一个和长条目标强相关的预热策略:如果训练集里绝大多数黄线框宽度超过 400 像素,模型前几十轮可能会出现 loss 下降很快但 mAP 不动的假象。原因是 gt 框太大,预测框稍微偏一点 IoU 就掉到 0.5 以下,mAP 计算不出来。这时先检查所有标注框的宽高分布,做一次可视化:
# 统计训练集所有黄线框的宽高比,确认是否已经切分 import os import numpy as np ratios = [] txt_dir = 'yolo_annotations' for fn in os.listdir(txt_dir): with open(os.path.join(txt_dir, fn)) as f: for line in f: vals = line.split() if vals[0] == '3': # yellow_line 类别 id w = float(vals[3]) h = float(vals[4]) if h > 0: ratios.append(w / h) print('max_ratio:', np.max(ratios)) print('median_ratio:', np.median(ratios))如果 max_ratio 大于 15,说明还有没切开的长框。这些框会让 mAP 卡住,别调参,先回去改标注。
4. 推理与标记:把 yolov11 输出转成机器人能用的路面地图
4.1 推理脚本:批量预测并保存 yolov11 标记结果
训练结束后,模型不能只活在验证集里,要让它对着一整段路测视频输出带标记的画面。这个步骤在项目里被称为“标记动作”,一方面是给班组里的机械和决策同事看效果,另一方面是生成机器的导航输入。用 ultralytics 的 API 写推理脚本非常直接:
import cv2 from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') def infer_video(video_path, output_path, result_json_path, conf_thres=0.35): cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) frame_id = 0 all_results = [] while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.predict(frame, imgsz=1280, conf=conf_thres, verbose=False)[0] boxes = results.boxes if boxes is not None and len(boxes) > 0: cls_ids = boxes.cls.cpu().numpy().astype(int) confs = boxes.conf.cpu().numpy() xyxy = boxes.xyxy.cpu().numpy() # 记录本帧结构化结果,供导航模块使用 for cid, conf, box in zip(cls_ids, confs, xyxy): all_results.append({ 'frame': frame_id, 'class': int(cid), 'conf': float(conf), 'bbox': [float(x) for x in box] }) # 画带标记的框,保存可视化视频 annotated = results.plot() writer.write(annotated) frame_id += 1 cap.release() writer.release() with open(result_json_path, 'w') as f: import json json.dump(all_results, f, indent=1) print(f"processed {frame_id} frames, saved {output_path}") infer_video('intersection_recording.mp4', 'prediction_output.mp4', 'detections.json', conf_thres=0.35)results.plot()默认会把类别名、置信度和框画在图上,这个方法最省事。但要注意 plot 出的图分辨率等于原视频分辨率,如果你的推理分辨率是 1280 而原视频是 4K,plot 的边框坐标是经过模型放缩后映射回原图的,残留的小瑕疵不影响展示,但做精度评估时不要用 plot 的像素结果,而是回到xyxy里取坐标。
保存成detections.json的意义在于对接导航模块,尤其是交通灯、左右转箭头这种离散事件。导航模块不需要看视频,只需要知道第 143 帧出现了一个左转箭头,置信度 0.82,位置在当前画面中心的左侧偏上。这样一个 JSON 文件就能完成感知和决策的解耦,排查问题时也方便按帧回放。
4.2 把“路面标记”翻译成导航指令:交通灯状态、左右转箭头与前方机器人
检测到框只是一步,真正让机器人跑起来的是把这些框翻译成语义指令。拿交通灯举例,yolov11 检测到的是“交通灯”这个整体目标,而不是灯的颜色。如果整个项目只建了一类 traffic_light,那么灯色判断需要在检测框内再做一次裁剪分类。常见做法是训练两个模型:一个 yolov11 检测灯的位置,一个轻量分类器判断红绿黄。也可以更粗暴一点,直接把交通灯按状态拆成三个类别,green_light、red_light、yellow_light。两种做法我都试过,单模型多类别在白天效果好,但夜间灯的颜色会被过曝变成白色,多分类模型更稳健。如果你要让机器人过路口,这个取舍值得提前定下来。
左右转类别相对简单,检测到 left_turn 框之后取它在画面中的位置——如果左转箭头在左侧 1/3 区域且车道线允许,就输出“准备左转”;如果右转箭头出现在右侧,就输出“右转”。关键点是箭头方向本身能不能用检测表达:我的做法是在类名上区分,left_turn 和 right_turn 分开建模,因为它们在 yaml 里是独立类别。这样一来,最后输出的导航指令就是 enum 形式,规控模块可以直接 switch-case。
前端机器人这一类比较特殊,它经常和静止的杆子混淆。如果一个机器人目标连续 30 帧出现在同一位置且置信度保持高位,我会优先怀疑它是一个黑色垃圾箱或者树桩,而不是真正移动的机器人。这是 5.3 节要展开的坑。
4.3 帧间信息融合:为黄线和人行道加一个轻量时序滤波
单帧检测结果用在导航上太跳了。黄线这一帧检测出三段,下一帧可能变成五段,再下一帧因为逆光可能只剩两段。为了让给路径规划模块的黄线位置稳定,我会在线后处理里加一个近似的时序滑动平均:保留上一帧所有黄线框,与当前帧黄线框按中心点距离做最近邻匹配,距离小于 30 像素的认为同一段线,直接对坐标取平均;新出现的框要连续出现 2 帧以上才对外输出,起到去抖作用。
import numpy as np prev_line_boxes = [] def temporal_smooth(current_boxes, prev_boxes, frame_threshold=2): matched = set() output = [] life_counter = {} for cur in current_boxes: if len(prev_boxes) == 0: output.append(cur) continue cx = (cur[0] + cur[2]) / 2.0 cy = (cur[1] + cur[3]) / 2.0 min_dist = 10**9 match_idx = -1 for j, prev in enumerate(prev_boxes): if j in matched: continue px = (prev[0] + prev[2]) / 2.0 py = (prev[1] + prev[3]) / 2.0 dist = (cx - px) ** 2 + (cy - py) ** 2 if dist < min_dist: min_dist = dist match_idx = j if match_idx >= 0 and min_dist < 30 ** 2: # 匹配上,对坐标做 0.5:0.5 平均 ave_box = (cur + prev_boxes[match_idx]) / 2.0 output.append(ave_box.astype(np.int32)) matched.add(match_idx) life_counter[match_idx] = 1 else: # 没匹配上,进入跟踪观察期 output.append(cur) return output这里默认每帧之间的目标移动不会超过 30 像素,对 5km/h 的机器人是成立的。如果你车速更快,距离阈值按速度换算。时序滤波不是万能的,它的代价是响应延迟两帧左右,对实时避障来说完全够用,但对“刹停”这种紧急动作,直接走单帧高置信度直通逻辑,不要等滤波。
5. 道路机器人路面标志识别的 4 个翻车场景与排查方法
5.1 黄线误检成马路边界,白线却丢了
现象是训练好的模型对黄色虚线区域的误检率特别高,大量白线被漏掉,同时预测置信度普遍高于正常水平。
原因是路面标志数据增强里开了默认的 fliplr 左右翻转,黄线在翻转后实际上变成了“左侧黄线”或“右侧黄线”,但黄色是不随翻转改变的,模型就会学到“只要有黄色长条就可能是黄线”。而白线因为颜色对比度低,加上 hsv_v 亮度增强幅度过大,容易被增强成灰色甚至黄色,语义就乱了。
解决方法是先关掉fliplr,对道路线类目标来说左右翻转会让车的行进方向语义错乱,左右转箭头更是会被水平翻转成错误方向。然后在数据清洗里抽查所有 hsv 增强后的样本,如果白线被染色成黄色,直接把它对应的图片移出训练集。我个人的习惯是fliplr=0.0,在路面标志项目里几乎不用它来扩充数据。
5.2 交通灯小目标在 640 分辨率下直接消失
现象是验证集 mAP 在 0.75 以上,但对着路口视频测试时,20 米外的交通灯完全没反应,40 米外更是预测不出任何框。
原因是交通灯目标在视野中只占大约 16x30 像素,经过 yolov11 的 32 倍下采样后,特征图里只有 0.5 个像素的部分被保留,信息已经无了。640 分辨率的模型对这类目标天然失效。
解决办法是三元组方案:一是训练和推理都上调分辨率,交通灯集中场景用 1280 或 1600 分辨率;二是把交通灯类别单独拿出来做一个小目标专用模型,输入分辨率用原始无压缩的 4K 裁剪区域;三是如果你不想上第二个模型,就给交通灯加上“灯架”上下文——把灯架整体作为目标标注,一个 1.2 米高的灯架在视野里是 80x120 像素,检测难度低很多。检测到灯架之后再用裁剪图判断灯色。这个方法的好处是没有增加模型数量,坏处是要求灯架在画面中稳定可见,树遮住灯架就废了。多数园区场景我还是推荐裁剪 + 分类的方案。
5.3 自己的机器人被识别成“前方机器人”
现象是机器人原地转弯时,模型偶尔把镜头视野边角里自己的机械臂或者机身外壳认成 robot 类,置信度有 0.5 到 0.7。这在避障决策里非常危险,可能让机器人在原地打转。
原因是数据标注时把“机器人”类别定义得太宽泛。标注团队把各种金属反光的长方形物体都标成 robot,比如配电箱、消火栓、路边栏杆的底座。模型学到了“有反光 + 长方体轮廓”就触发响应,而机器人自身的外壳恰好满足这两个特征。
解决方法是做一次严格的样本清理,把所有非移动体的 robot 样本删除或改标注成其他类,比如 background。如果这些静态物体确实需要避障,可以单独建一个 obstacle 类,不要和 robot 混在一起。同时推理时把画面中央下方的固定区域做掩膜,因为自己的机械臂在工程上只会出现在固定区域,这个区域直接不送入模型。还有一种做法是检查这一帧的检测框中心和上一帧是否在同一位置,如果连续 50 帧都完全没动,绝大概率是静态物体,导航模块应该忽略它,除非你在做“静止闸机阻挡”场景。
5.4 雨天反光导致人行道斑马线断成一截一截的
现象是晴天模型很好用,一到下雨后路面有积水,人行道斑马线的检测框数量变成晴天的三分之一,且集中在反光最强的几个位置。
原因是雨水反光改变了斑马线的局部纹理。斑马线本身是白色条纹,积水造成高光区域,模型提取的纹理特征变成“白色高反射”而不是“规则条纹”。如果标数据时只用了晴天素材,模型典型表现是伪影误检成斑马线的一部分,真实条纹反而因为周围高对比被抑制。
解决方法是给训练集加湿度扰动。做法是对原图做 gamma 变换和 CLAHE 对比度增强,模拟雨天弱对比度的图像。更实用的是在标注阶段收集低光照、反光场景的数据,单独加 20% 到 30% 的雨天样本。模型训练时,这些样本会和普通样本形成平衡,避免只学到晴天纹理。推理端还有一个技巧:如果检测目标是斑马线整片区域,但检测框是断开的,对宽度方向做膨胀后接并集,再把同一行的框合并成整条斑马线,这个几何后处理能有效抗断检。
6. 从 yolov11 标记结果到道路机器人闭环:Jetson 部署技巧与真实验证
6.1 导出 TensorRT 引擎,在 Jetson Nano 上跑出稳定延迟
模型验证通过之后,部署阶段我一般直接把权重导出成 TensorRT engine。命令很固定:
yolo export model=runs/detect/train/weights/best.pt format=engine half=True imgsz=1280 device=0关键是half=True,在 Jetson Orin Nano 上 FP16 推理比 FP32 快接近一倍,精度损失在路面标志这个场景通常只有 0.5 个点以内。导出后写一个小的加载脚本检查延迟:
yolo predict model=best.engine source=intersection_test.mp4 imgsz=1280 device=0如果延迟在 30ms 以上,优先检查是不是imgsz=1280导致的。实际上很多路面标志并不需要全程 1280,比如空旷直道上的黄线,640 就足够了。常见做法是保持一个 1280 的模型用于路口和近距离标志,另一个 640 模型用于低速巡线,两个模型交替执行。这套配置在 Jetson Orin Nano 上能把平均推理延迟维持在 25ms 左右。
6.2 用现场重放验证:不只看 mAP,还要看误报率
很多 yolov11 用户习惯用 ultralytics 的 val 命令算一堆指标,然后觉得模型没问题。但对道路机器人来说,mAP 高不等于能跑,因为 mAP 对类别不平衡不敏感,对同一目标连续多帧的稳定性也不展示。我建议做一次现场重放验证:把录制的半小时路测视频按时间戳切片段,让模型离线跑一遍,统计每一类的检测次数、连续漏检帧数以及“幽灵检测”出现次数。
import json with open('detections.json') as f: dets = json.load(f) # 统计每个类别的帧级出现情况,找出超过 10 帧连续丢失的目标 from collections import defaultdict class_appear = defaultdict(list) for d in dets: class_appear[d['class']].append(d['frame']) for cls_id, frames in class_appear.items(): missed = [] last = frames[0] for idx in range(len(frames)): diff = frames[idx] - last if diff > 10: missed.append((last, frames[idx], diff)) last = frames[idx] print('class', cls_id, 'detections:', len(frames), 'missed_gaps>10f:', len(missed))这个脚本输出的就是每个类别连续丢帧的区间,丢帧超过 1 秒(25到30帧)的都要拉出来看原因。很多时候 mAP 全是 0.95,但某个路口左转箭头隔三差五丢 20 帧,机器人就会错过转弯机会。这种验证比优化 mAP 本身更接近落地需求。
6.3 地面实况数据回采的工具与习惯
最后一个具体技巧是关于数据集闭环的。路面标志识别项目永远不会缺“新坑”,比如某个路口的黄线被重新划线,或者新装了带倒计时数字的交通灯。我一般会在每一次路测后保留原始未裁剪视频,以及模型输出的 JSON。每周做一次增量训练:把本周检测置信度低于 0.4 的“漏检嫌疑”,和高于 0.8 但导航反馈是误报的样本挑出来,人工确认后补标注进数据集,重新训练 50 到 80 个 epoch。这个习惯让模型每两周就能吸收一批新场景,而不是一次训练后用到坏。
这套方法的投入产出比很高,尤其适合路面标志这种类别固定、但外观因时间和天气持续漂移的感知任务。从数据准备到部署验证,yolov11 的能力边界和翻车点都在可控范围内。做这项工程给我最大的教训,是不管模型训练指标多漂亮,都要先跑一百遍现场录像再去上机器人,因为道路场景里的意外永远比代码里的 bug 多。希望帮到你。
本文还有配套的精品资源,点击获取