简介面向智能小车赛道自动驾驶场景的交通指示牌目标检测数据集覆盖左转、右转、红灯、绿灯、人行道等8个类别图像为200×120的RGB图片。压缩包约10.31MB共2000个文件以1127个txt标注、872张jpg图像和1个标签可视化py脚本为主。数据按YOLOV5标准目录组织训练集901张图片及对应标签、验证集225张图片及对应标签并附带类别名称字典无需额外处理即可直接训练。可视化脚本可随机读取一张图片绘制边界框并保存到当前目录便于快速核验标注质量。目前已有151人学习适合智能车竞赛、自动驾驶课程实验及YOLOV5目标检测入门实践的开发者直接使用。1. 赛道视觉不是通用目标检测这套8类别数据集的定位与边界做智能小车赛道自动驾驶时很多开发者第一反应是拿一个在 COCO 上预训练的通用检测模型直接跑结果发现红绿灯时而能识别、时而消失转弯标志在小分辨率下几乎不可用最后不得不自己从零整理数据。这套 8 类别目标检测数据集解决的就是这个具体问题它按 YOLOV5 目录格式组织类别只覆盖赛道自动驾驶真正需要的交通指示牌与路况元素拿来就能直接进train.py训练不涉及格式转换也不需要为 80 类通用目标付出额外的计算代价。适合做智能车竞赛、课程设计以及低成本自动驾驶验证平台的从业者既能跑通流程也能看清赛道视觉的数据边界。2. 从原始素材到YOLOV5标准目录标注规范与8类标签体系2.1 为什么是YOLOV5目录格式目录结构与txt标注的对应关系YOLOV5 的目录格式之所以成为事实标准是因为它把「图片」和「标注」做成了严格一一对应的两个平行目录训练器不需要任何数据库或 JSON 中间层直接按文件名匹配即可。这个设计对数据集整理者非常友好任何能产出同名 txt 的标注工具导出后都能直接复用。dataset/ ├── images/ │ ├── train/ # 训练图像约 80% │ └── val/ # 验证图像约 20% ├── labels/ │ ├── train/ # 与 images/train 一一对应的 txt 标注 │ └── val/ └── data.yaml # 训练时读取的数据配置图片和标注文件必须同名例如frame_0012.jpg对应frame_0012.txt后缀不同没关系前缀必须完全一致。这种约定把「一张图有哪些目标」直接沉淀为一行行文本人眼可读也方便脚本做批量检查。txt 文件中每一行代表一个目标框五个字段分别是类别 ID、归一化中心 x、归一化中心 y、归一化宽度 w、归一化高度 h全部取值在 0 到 1 之间。下面是一份真实样本中的两行标注0 0.628906 0.351562 0.089844 0.135417 4 0.732812 0.802083 0.053125 0.070833第一行表示类别 0红灯中心点在图像 62.9% 宽度、35.2% 高度的位置框宽约为整图宽度的 8.98%框高约为整图高度的 13.54%。归一化坐标的意义在于无论训练时把图像 resize 到 640 还是 320框的相对位置都不会漂移模型不会因为输入分辨率变化而读到错位的标注。标注工具端通常导出的是像素坐标落地前需要先做一次除以图像宽高的归一化这一步出错会导致训练时损失剧烈抖动后面第 5 章会专门展开。2.2 8个类别怎么定赛道场景的标签体系设计逻辑这类数据集最常见的误区是把类别定义得跟通用目标检测一样宽结果每个类别只有几十张图模型完全学不过来。赛道自动驾驶的场景高度受限视觉需要回答的问题其实只有几类现在能不能走、要不要减速、往哪边转、前方有没有障碍。8 个类别的划分应当围绕这几个决策点展开而不是追求大而全。类别 ID名称语义对应驾驶决策0red_light红灯停车等待1green_light绿灯正常通行2speed_limit限速标志降速3stop_line停车线/停止标志制动4turn_left左转标志左转5turn_right右转标志右转6road_block路障/三角锥避障或停车7zebra_crossing斑马线减速观察这个分类里红绿灯被拆成两个独立类别而不是一个traffic_light笼统概括因为小车控制逻辑需要直接知道灯色。左右转标志也拆开避免模型只学会「有个标志」而分不清方向。真实赛道若出现黄灯可以替换某个低频类别或扩到 9 类但有一个原则需要守住类别一旦写入data.yaml训练集、验证集、推理脚本必须全链路保持一致否则会出现检测框正确但类别名错乱的情况。2.3 数据划分与目录落盘最小可运行的整理脚本拿到原始图片和对应标注后第一步不是急着训练而是先按固定随机种子完成数据集划分。我一般按 8:2 切分训练集和验证集保证同类别的分布尽量均匀避免某个类别的图片全部落在验证集里。划分脚本的核心是「按文件名复制到目标目录」同时检查图片和标注是否成对出现。import os import shutil import random # 配置三个路径 raw_images raw/images # 原始图片目录文件名与标注 txt 同名 raw_labels raw/labels # 原始 txt 目录 out_root dataset # 输出的 YOLOV5 标准目录 random.seed(42) # 固定随机种子保证每次划分结果一致 all_imgs sorted(os.listdir(raw_images)) random.shuffle(all_imgs) val_num int(len(all_imgs) * 0.2) # 验证集比例 20% for split in [train, val]: os.makedirs(f{out_root}/images/{split}, exist_okTrue) os.makedirs(f{out_root}/labels/{split}, exist_okTrue) for idx, img_name in enumerate(all_imgs): split val if idx val_num else train stem os.path.splitext(img_name)[0] # 图片和标注必须成对存在缺失要报警告 if not os.path.exists(f{raw_labels}/{stem}.txt): print(fWARN: {stem} 缺少标注文件跳过) continue shutil.copy(f{raw_images}/{img_name}, f{out_root}/images/{split}/{img_name}) shutil.copy(f{raw_labels}/{stem}.txt, f{out_root}/labels/{split}/{stem}.txt) print(划分完成训练集与验证集图片数量) for split in [train, val]: count len(os.listdir(f{out_root}/images/{split})) print(split, count)val_num取值是关键参数如果你只有 200 张图20% 就是 40 张验证集会偏小mAP 波动较大。此时可以下调到 10%或者改用 K 折交叉验证来评估。脚本中的random.seed(42)保证了多次执行结果一致这在对比实验时很重要否则模型效果差异会混入数据划分的随机性让你误判算法改进的效果。3. 用这套数据集训练YOLOV5s环境、命令与参数3.1 数据配置YAML告诉训练器数据在哪、有几类把目录结构整理好之后还需要一份data.yaml告诉训练器数据集的位置和类别定义。这个文件是整个训练流程的「地图」路径写错、类别顺序写乱都会直接导致训练失败或推理结果错乱。path: dataset # 数据集根目录相对于运行训练命令的目录 train: images/train val: images/val nc: 8 # 类别总数 names: 0: red_light 1: green_light 2: yellow_light 3: stop_line 4: speed_limit 5: turn_left 6: turn_right 7: road_blockpath字段推荐写成相对路径这样整个数据集文件夹可以整体迁移到别的机器。train和val是相对path的二级目录。names中的 ID 顺序必须与标注 txt 第一列的数字严格对齐这里如果写错模型训练时显示的类别名和实际框内容不匹配且不容易被察觉。注意nc要和names列表长度一致这是检查频率最高的低级错误。3.2 启动训练关键超参数与显存适配训练命令的选型上我推荐直接用 YOLOV5 官方仓库中的train.py它已经把数据加载、增强、评估全部封装好了。对小车赛道这种中等规模数据集yolov5s是性价比最高的起点比yolov5n准确率高又比yolov5m训练速度快得多。python train.py \ --data race_track.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0--img 640是输入分辨率赛道小目标较多时这个值不建议小于 512。--batch 16在单张 RTX 3060 级别的显卡上刚好占满约 8GB 显存如果训练时出现 CUDA OOM先把 batch 减半而不是去减小图片分辨率因为分辨率下降对小目标检测的影响更直接。--epochs 100对几百张规模的数据集通常足够配合默认的patience100早停机制模型会在验证集指标不再上升时自动停止不会空耗时间。训练过程需要盯三个输出loss曲线是否持续下降、mAP0.5是否逐步上升、results.png中验证集的预测框和真实框对比。如果 loss 在前 20 轮就降到接近 0.02 左右多半是标注有问题而不是模型学得好这时要停下来做数据自检而不是继续等训练结束。3.3 权重导出与推理脚本把模型搬到小车上训练完成后runs/train/exp/weights/best.pt就是验证集上表现最好的权重。小车端推理最常见的方式是直接加载这份权重做前向传播。实际部署时可以把权重转换成 TorchScript 格式省掉依赖环境不一致带来的麻烦但调试期直接用.pt文件更方便可视化。import torch import cv2 # 使用本地 YOLOV5 仓库的加载接口读取训练权重 model torch.hub.load( yolov5, # 本地仓库目录名 custom, pathruns/train/exp/weights/best.pt, sourcelocal ) # 读取小车回传的摄像头帧 img cv2.imread(frame_0012.jpg) results model(img, size640) # 输出 JSON 友好的结果方便下游决策逻辑读取 for det in results.pandas().xyxy[0].itertuples(): print(det.name, round(det.confidence, 3), int(det.xmin), int(det.ymin), int(det.xmax), int(det.ymax))推理时size参数必须和训练时的--img保持一致否则会引入尺度分布偏移。这里逐行打印出类别名、置信度、边框坐标小车下位机可以直接解析 stdout 做决策。如果推理速度不达标优先把输入降到 480 或 416 观察精度损失而不是盲目换更轻量的模型。4. 数据集自检与增强让8类样本真正可用4.1 标注质量自检越界框与类别ID的快速排查标注错误是训练效果差的头号原因。与其等训练完去猜为什么 mAP 低不如在训练前写一个几秒钟就能跑完的自检脚本把所有可疑标注一次性找出来。这里需要检查四类问题字段数是否为 5、类别 ID 是否在 0~7 之间、中心点是否越界、框是否超出图像边界。import os from collections import Counter label_dir dataset/labels/train valid_categories set(range(8)) # 8 类编号 0~7 stats Counter() bad_files [] for file in os.listdir(label_dir): if not file.endswith(.txt): continue path os.path.join(label_dir, file) with open(path) as f: for line in f: parts line.strip().split() # 空行直接跳过不代表错误 if len(parts) 0: continue if len(parts) ! 5: bad_files.append((file, 字段数不是5)) continue cat float(parts[0]) cx, cy, w, h (float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])) if int(cat) not in valid_categories: bad_files.append((file, f类别ID非法:{cat})) # 中心点必须在[0,1]范围内 if not (0 cx 1 and 0 cy 1): bad_files.append((file, 中心点越界)) if w 0 or h 0: bad_files.append((file, 宽高异常)) # 框的四个顶点都不能超出图像范围 if cx w / 2 1 or cy h / 2 1 or cx - w / 2 0 or cy - h / 2 0: bad_files.append((file, 边界框超出图像范围)) stats[int(cat)] 1 print(类别分布:, dict(stats)) if bad_files: print(异常样本:) for bad in bad_files[:40]: print(bad) else: print(未发现明显异常)类别分布输出能帮助你一眼看出类别不平衡程度。如果road_block有 500 个框而zebra_crossing只有 20 个框训练结果几乎必然出现斑马线漏检。异常样本列表则直接给出问题文件路径配合标注工具打开就能定位。这个脚本建议在每次新增数据后都跑一遍哪怕只加了几十张图也可能把之前没有的标注错误带进来。4.2 赛道场景的增强策略别把增强加过头YOLOV5 内置了相当强的数据增强默认配置对 COCO 这种大而杂的数据集效果不错但对赛道场景反而需要收敛。原因很简单赛道图像高度受控目标通常是正对摄像头的平面标志过度的几何增强会制造出训练分布中根本不存在的样本。# train.py 中通过 hyp 配置文件调整增强超参数 mosaic: 0.8 # 把 4 张图拼成 1 张对小目标有帮助 mixup: 0.2 # 两张图叠加提升鲁棒性但不能开太大 fliplr: 0.0 # 关闭随机水平翻转左右转标志翻转后会语义错乱 hsv_h: 0.01 # 色调变化幅度减小赛道灯光颜色是关键特征 hsv_s: 0.4 hsv_v: 0.4fliplr必须设为 0这是赛道场景最容易踩的坑turn_left标志水平翻转后图像语义变成了turn_right但 txt 里的类别 ID 没变等于用错误标签训练。mosaic建议保留 0.8因为赛道小目标占比高拼接图能增加目标尺度的多样性。hsv增强幅度也要克制红灯绿灯的判别高度依赖颜色过度调整色调会让灯色在增强图中失真。5. 实战避坑训练阶段最容易翻车的5个问题这个领域的水很深很多问题不是模型结构不行而是数据和训练配置在细节上出了问题。下面这 5 个问题是我在不同小车项目上反复遇到过的按「现象 → 原因 → 解决」逐条拆开希望能帮你少走弯路。5.1 现象红绿灯漏检反复出现训练时总 loss 正常下降验证集 mAP 也不算低但小车实跑时红灯经常漏检绿灯却表现正常。这类问题首先要看类别分布。原因通常是红灯样本远少于绿灯模型把学习容量都花在了高频类别上低频类别的决策边界学得不到位。解决方法是统计类别框数后做针对性补充优先收集红灯在顺光、逆光、不同距离下的照片如果暂时补不到数据可以在训练时提高该类的损失权重让模型更关注少数类。5.2 现象小尺寸指示牌全部丢失speed_limit和turn_left这类标志在远处时可能只有十几个像素经过网络的逐步下采样后特征几乎消失。现象是 mAP0.5 尚可但 mAP0.5:0.95 很低小车接近标志前一直没有输出。原因是在 640 分辨率下小目标的有效像素太少。解决方向有两个一是把训练分辨率提到 768 或 896 再配合切片推理二是把远近样本单独抽出来做一次数据平衡确保训练集里小尺寸目标的占比不低于真实赛道场景。5.3 现象mAP不错但小车实跑丢目标验证集上 mAP0.5 超过 0.9但小车实际跑起来目标框会闪烁连续几帧丢失后再恢复。这通常是训练数据与实拍数据的分布差异造成的赛道现场有运动模糊、镜头畸变和不同角度的光照而训练集图片大多来自相对干净的采集。解决方法是收集一段实跑视频按帧抽图补充到数据集中并在推理端加入帧间平滑机制例如用最近 3 帧的检测结果投票决定最终输出比单纯调低置信度阈值更有效。5.4 现象训练损失不下降训练初期 loss 一直在 0.1 附近波动甚至不降反升。常见原因有两个一是 txt 标注坐标没有归一化中心点和宽高写成了像素坐标数值范围远超 0~1二是标注框越界某个目标的中心点或宽高超出了图像范围。解决方法是先跑一遍第 4.1 节的自检脚本把越界样本全部修正后再训练。这个问题的隐蔽之处在于训练不会报错loss 也不会发散到 NaN只会表现为缓慢的波动很容易被误判成模型或学习率问题。5.5 现象推理结果类别张冠李戴模型能框出目标但把红灯识别成绿灯或者把限速标志识别成停车标志。这类问题通常是类别 ID 顺序不一致造成的标注时用的names顺序和推理时模型加载的类别顺序不同。比如标注文件里 ID 2 是speed_limit但data.yaml里 ID 2 写的是stop_line模型输出的 2 号结果就会显示成停止线。解决方法是统一所有环节的names定义并且训练完成后把权重放回验证集逐类打印预测框检查一遍不要只看整体 mAP。6. 最后的技巧用视频回放做硬例挖掘补数据比换模型更有效模型已经跑得不错但总有几个目标在特定角度、特定光照下检测不到。与其重新采集大量数据不如把小车实跑的视频拿回来做一次简化版的硬例挖掘针对性补充那些「模型没把握但真实存在」的样本。6.1 低置信度帧自动筛选脚本思路是让训练好的模型以低置信度阈值跑一遍真实赛道视频把置信度落在中间区间比如 0.10~0.35的帧自动保存下来。这些帧就是典型的困难样本人工只需打开图片确认目标是否存在并修正标注就能以很小的代价显著提升模型的短板能力。import cv2 import torch import os video_path recordings/run_01.mp4 cap cv2.VideoCapture(video_path) # 加载训练好的最佳权重 model torch.hub.load( yolov5, custom, pathruns/train/exp/weights/best.pt, sourcelocal ) out_dir hard_examples os.makedirs(out_dir, exist_okTrue) frame_idx 0 while True: ret, frame cap.read() if not ret: break results model(frame, size640) # 保持和训练一致的尺度 df results.pandas().xyxy[0] # 目标有响应但置信度不高认定为可疑困难样本 hard df[(df.confidence 0.10) (df.confidence 0.35)] if len(hard) 0: cv2.imwrite(f{out_dir}/frame_{frame_idx:06d}.jpg, frame) # 保存一份低置信度标注便于人工修正后直接并入训练集 with open(f{out_dir}/frame_{frame_idx:06d}.txt, w) as f: for det in hard.itertuples(): x1, y1, x2, y2 det.xmin, det.ymin, det.xmax, det.ymax # 像素坐标转 YOLO 归一化格式 cx (x1 x2) / 2 / frame.shape[1] cy (y1 y2) / 2 / frame.shape[0] w (x2 - x1) / frame.shape[1] h (y2 - y1) / frame.shape[0] f.write(f{int(det.class)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n) frame_idx 1 cap.release() print(f处理完成共扫描 {frame_idx} 帧保存疑似困难样本到 {out_dir}/)0.10~0.35这个置信度窗口不是固定的如果你的模型整体置信度偏高就把窗口上调到0.25~0.5反之则下调。人工修正时重点看两类情况一是模型把背景误识别为目标的假正例二是目标真实存在但被漏掉的真负例。把人工修正后的帧增量并入训练集再复训一次往往比直接换大模型带来的收益更直接。补数据这件事我曾经迷信过换模型结构后来发现对赛道这类小数据集真正决定上限的是困难样本的覆盖度。一个简单的习惯是每次实跑都把视频存下来标记场景和失误点回到工位后按这套流程挖一轮数据。坚持三轮之后你会发现同样的模型结构 mAP 涨了好几个点这种效果是靠调参很难换来的。希望这套流程能帮你在智能小车赛道上少走点弯路把精力放在真正影响落地效果的地方。本文还有配套的精品资源点击获取