☰
红绿灯目标检测数据集实战:从VOC/YOLO格式转换到YOLOv8训练全流程
2026/9/28 15:48:55 网站建设 项目流程

简介:YOLO红绿灯目标检测数据集采用真实道路场景中的高质量图片,面向目标检测初学者、课程实训及算法调优人员,解决红绿灯识别训练中标注数据不足、格式不统一的问题。数据使用LabelImg标注,标注框质量高,并已整理为VOC、COCO和YOLO三种格式标签,分别存放于独立文件夹,可直接用于YOLO系列模型训练。整套资源共2000个文件,核心由xml标签、txt标签和yaml配置组成,另有html环境搭建与训练教程、py数据集划分脚本,压缩包大小约487.95MB。配套内容覆盖Linux与Windows双平台的YOLO环境搭建、训练案例讲解,以及将图片和标签自动划分到训练集、验证集、测试集的工具脚本,方便按项目需求灵活拆分数据。目前已有863人学习下载,适合希望以完整真实场景数据快速上手YOLO红绿灯检测任务的用户。

1. 红绿灯目标检测数据集:拿来就能训,但先别急着解压

前阵子接了个智慧交通的活,要在路口摄像头画面上把红绿灯检测出来,红灯、绿灯、黄灯三种状态还得分开。模型好说,YOLO 系列挑一个就行,难的是数据——公开的目标检测数据集里交通标志一大堆,红绿灯真正能用的很少,要么是欧洲路口风格,要么是无人机俯拍,跟国内平视摄像头拍出来的画面差很远。后来我搞到一份红绿灯目标检测数据集,真实场景图片配齐了 VOC、COCO、YOLO 三种格式标签,还带了划分脚本和训练教程。这篇文章就把这个资源拆开讲清楚:三种标签怎么对、划分脚本该用哪个、训练参数怎么设、哪些坑必须绕开。适合正在做自动驾驶感知、智慧交通项目,或者刚学 YOLO 想找个能直接落地数据集练手的工程师。

2. 三种标签格式:VOC、COCO 与 YOLO 的差异,以及一个能抄走的转换脚本

2.1 为什么同一份数据要做成三种格式

解压压缩包后,你会看到标签被分成了三个文件夹,分别对应 VOC、COCO、YOLO 三种格式。第一次用的人容易犯嘀咕:为什么不统一成一种?因为不同训练框架和代码库认的格式不一样。YOLO 系列原生用的是 txt,每行一个目标;SSD、Faster R-CNN 这类老代码常读 VOC 的 xml;mmdetection、Detectron2 则习惯吃 COCO 的 json。同一份数据切成三种格式,本质上就是让你拿到任何项目都能直接塞进去训练,不用再折腾一遍转换。

三种格式的核心差异在于边界框的表示方式。VOC 的 xml 记录的是像素绝对坐标,也就是 xmin、ymin、xmax、ymax 四个整数,单位是像素,读起来直观但文件体积大,一个 xml 要嵌套一堆节点。COCO 的 json 里 bbox 字段是 [x, y, width, height] 的浮点数组,x、y 是框左上角坐标。YOLO 的 txt 则是归一化坐标,一行一个目标,五个数字依次是类别 ID、中心点 x、中心点 y、框宽、框高,全部除以图片宽高,范围在 0 到 1 之间。

格式文件后缀bbox 表示典型使用场景训练时需要额外注意
VOCxmlxmin, ymin, xmax, ymax(像素)老版 Faster R-CNN、SSD需解析 xml 树
COCOjsonx, y, width, height(像素)mmdetection、Detectron2需按 categories 匹配 ID
YOLOtxtx_center, y_center, w, h(归一化)YOLOv5、YOLOv8、YOLOv11类别 ID 与 names 顺序强相关

这里有个小细节:压缩包里说明文档把标注软件写成了 “lableimg”,实际是 LabelImg,文档错别字,不影响使用。但你拿到数据后建议先自己做一次质量校验,别只信说明文档。

2.2 先用脚本确认数据质量:统计各类目标数量

训练之前我习惯先统计一遍各类目标框数量,心里有底再决定要不要做类别平衡。YOLO 的 txt 标签最好统计,每行代表一个目标。下面这段脚本扫一遍标签目录,输出每个类别的框数:

import os label_dir = "labels/train" # 改成你的实际标签路径 class_names = ["red", "green", "yellow"] # 以数据集实际的 names 顺序为准 counts = {name: 0 for name in class_names} total_boxes = 0 for txt_name in os.listdir(label_dir): if not txt_name.endswith(".txt"): continue with open(os.path.join(label_dir, txt_name), "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue cls_id = int(parts[0]) if cls_id >= len(class_names): continue counts[class_names[cls_id]] += 1 total_boxes += 1 print("总目标框数:", total_boxes) for name, cnt in counts.items(): print(f"{name}: {cnt}")

这段代码的逻辑很简单:遍历标签目录下所有 txt,每行按空格拆成五段,第一段是类别 ID,用它去 class_names 里查名字。注意这里假设了类别 ID 从 0 开始且顺序固定,如果数据集里黄灯样本特别少,后面划分数据集时就要小心。统计结果出来后,如果红灯两千个、绿灯一千五、黄灯只有两三百,那就别指望黄灯能训出高精度,这种真实场景数据的不均衡是常态,不是模型 bug。这个数据集因为是真实路口采集,大概率存在类似情况,先有预期,后面才不会慌。

2.3 自己标完新图怎么转:VOC XML 转 YOLO TXT 的脚本

这份资源本身三种格式都齐了,直接用即可。但等你上手跑完一轮,大概率会想往数据集里补自己的图片,LabelImg 默认存的是 VOC xml,这时候就得转成 YOLO txt。坐标转换的核心是归一化公式:x_center 等于 (xmin + xmax) 除以 2 再除以图片宽度,宽高同理。

import os import xml.etree.ElementTree as ET from glob import glob def voc_to_yolo(xml_file, out_dir, class_names): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) txt_name = os.path.splitext(os.path.basename(xml_file))[0] + ".txt" lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: continue cls_id = class_names.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(os.path.join(out_dir, txt_name), "w", encoding="utf-8") as f: f.write("\n".join(lines)) class_names = ["red", "green", "yellow"] # 改成你的实际类别名 os.makedirs("labels_out", exist_ok=True) for xml_file in glob("annotations/*.xml"): voc_to_yolo(xml_file, "labels_out", class_names)

这段脚本里有个容易忽略的判断:if name not in class_names。你标注的时候可能顺手标了别的物体,比如行人、车辆,训练红绿灯模型时这些目标不应该参与,直接跳过最省事。另一个要注意的点是 LabelImg 导出的 xml 里一定有size/width和size/height,但如果你用其他工具生成过 xml,结构可能略有差异,建议转换前打印一根 xml 看看字段名。

3. 划分训练集、验证集、测试集:三个脚本的分工、顺序和参数

3.1 先分清三个脚本:哪个复制文件、哪个只生成列表

压缩包里带三个划分脚本,名字很像,第一次接触的人很容易混:一个叫“训练集、验证集、测试集划分脚本(图片标签划分写入新文件夹).py”,一个叫“训练集、验证集划分脚本(图片标签划分写入新文件夹).py”,还有一个叫“split_train_val 生成 ImageSets 下 txt 文件划分脚本.py”。三者的核心区别在于:前两个会把图片和标签物理复制到新文件夹,第三个只生成一行行图片路径的 txt 列表文件,不复制任何数据。

脚本输出结果是否复制文件适合的训练方式
train/val/test 三份划分三个独立文件夹是YOLOv5/YOLOv8 的 data.yaml 方式
train/val 两份划分两个独立文件夹是不需要测试集的场景
split_train_val 生成 txt若干 txt 列表文件否老式 VOC 训练流程

如果你打算用 YOLOv8 或 YOLOv5,推荐先用“复制到新文件夹”的脚本,因为 data.yaml 里直接写train: images/train就能用,干净利落。split_train_val 那个脚本是给需要手动读列表的代码准备的,用得少但存在即合理。下面重点讲两个复制型脚本怎么调参数。

3.2 推荐先跑的脚本:训练集/验证集/测试集划分脚本

我一般优先用三份划分的脚本,因为测试集从一开始就独立出来,后面验证模型泛化能力才有依据。这类脚本的核心逻辑是:读入所有图片文件名,按比例随机打乱,用 random.shuffle,再把对应图片和标签复制到目标目录。脚本开头通常是这样的结构:

import os import random import shutil random.seed(42) # 固定随机种子,每次划分结果一致 train_ratio = 0.7 val_ratio = 0.2 test_ratio = 0.1

注意这个 train_ratio、val_ratio、test_ratio 三个值加起来必须等于 1,否则脚本会报错或者少一类数据。如果数据集只有几百张,建议把测试集比例降到 0.1,把更多数据留给训练;一千张左右的话 7:2:1 是合理选择。random.seed(42) 这行特别重要,不设置随机种子的话,每次运行划分结果都不一样,后面排查问题时分不清是数据变了还是代码变了。

脚本运行前要改三处路径:图片源文件夹、标签源文件夹、输出根目录。图片和标签必须放在不同的两个源文件夹里,比如 images/ 和 labels/,脚本按同名前缀去找对应文件。这个数据集里图片和标签是配套的,不会出问题,但你自己扩充数据后就要注意同名检查,最稳妥的做法是先跑一段校验代码,找出只存在图片或者只存在标签的文件名:

import os img_dir = "images" label_dir = "labels" img_names = {os.path.splitext(f)[0] for f in os.listdir(img_dir)} label_names = {os.path.splitext(f)[0] for f in os.listdir(label_dir)} print("缺标签的图片:", img_names - label_names) print("缺图片的标签:", label_names - img_names)

这一跑如果有输出,先把数据补齐了再划分,不然后面训练会遇到图片有标签但对不上框这种玄学问题。

3.3 只生成列表文件的 split_train_val 脚本:什么场景才需要

这个脚本生成的 txt 文件每行是一个图片路径,格式类似JPEGImages/traffic_001.jpg。老版本的 YOLOv3、YOLOv4 训练经常需要这种列表,YOLOv5 之后已经不太用了。如果你手头的代码还是读 txt 列表的老流程,可以跑它生成 train.txt、val.txt、trainval.txt。

跑这个脚本之前先确认一件事:txt 里的路径相对于谁的根目录。脚本内部通常写的是类似JPEGImages/xxx.jpg的短路径,你需要保证训练代码的工作目录能根据这个相对路径找到图片。很多人在这里翻车,生成完 txt 之后发现路径前面缺了一截,训练一启动就报错找不到图片。对策是生成后用文本编辑器打开 train.txt,随机抽三行,手动去文件系统里验证路径能否对得上。

如果你的最终目标是 YOLOv8,这个脚本基本不需要碰,知道它是干嘛的就行。真正要避免的错误是把复制型脚本和列表型脚本混着用——先复制完数据又跑列表脚本,列表里的路径还是老路径,配合训练代码一读就乱,这属于典型的顺序灾难。

4. 从环境搭建到跑通 YOLO 训练:以 YOLOv8 为例的完整流程

4.1 环境搭建:Windows 与 Linux 的差异

资源包里有四个 HTML 教程,分别覆盖 Windows、Linux 的环境搭建和训练流程。两个系统的本质区别不大,核心就三步:装 Anaconda、建虚拟环境、装 PyTorch 和 ultralytics 包。Linux 服务器上我一般这样建环境:

conda create -n yolo python=3.9 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics

解释一下这几条命令:第一条创建名为 yolo 的虚拟环境,指定 Python 3.9,避免和系统 Python 环境互相污染;第二条激活环境;第三条装 GPU 版 PyTorch,cu118 表示 CUDA 11.8 对应的版本,如果服务器 CUDA 是 12.x,把 cu118 换成 cu121;第四条装 YOLOv8 的官方包。Windows 上操作一样,只是命令行换成 Anaconda Prompt,路径反斜杠多一些。没有 NVIDIA 显卡也能跑,把 torch 装成 CPU 版,模型能训练,速度慢几十倍,适合先走通流程。

装完以后必须验证 GPU 状态:

python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"

输出第一行是 PyTorch 版本号,第二行输出 True 说明 GPU 可用,输出 False 说明装的 CPU 版或者显卡驱动不对。这里卡住最常见的原因是 CUDA 驱动版本太老,PyTorch 要求的驱动和系统驱动不匹配。实在装不上 GPU 版就先跑 CPU 版,别在环境上耗太久。

4.2 整理目录结构并修改 data.yaml

用划分脚本跑完之后,数据目录应该是这样的结构:

datasets/traffic_light/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/

YOLOv8 训练时会在 images 的同级目录找 labels,自动读取同名 txt,不需要你在配置文件里写标签路径。对应的 data.yaml 这样写:

path: D:/datasets/traffic_light # 数据集根目录,改成你的实际路径 train: images/train val: images/val test: images/test names: 0: red 1: green 2: yellow

path 是根目录,train、val、test 都是相对 path 的路径。这里有个细节必须强调:YOLO 的标签 txt 里第一行数字如果是 0,那就对应 names 里的第一个类别,因此 names 里的顺序不能乱写。很多人在这一步把 red 写在 1 的位置,训练完预测时红灯变绿灯,就是这个原因。另外 Windows 路径里反斜杠容易出转义问题,yaml 里统一用正斜杠或者双反斜杠,路径不要带中文,这是老生常谈但每次都有人踩。

4.3 启动训练:epochs、batch、imgsz 怎么定

环境就绪、yaml 改好之后,训练命令其实很短:

yolo detect train data=traffic_light.yaml model=yolov8s.pt epochs=150 imgsz=640 batch=16

model 参数可以填预训练权重文件路径,yolov8s.pt 是轻量级版本,红绿灯检测用 s 足够了。追求精度可以换 yolov8m.pt,但训练时间和显存占用会上升。epochs 设 150 轮,红绿灯任务不算复杂,100 到 150 轮基本能收敛,再大容易过拟合。imgsz 是输入图片分辨率,默认 640,如果画面里红绿灯目标很小,可以调成 960,代价是训练变慢。batch 大小取决于显存,6G 显存就设 8,12G 可以设 16,装完环境不确定就先从 4 开始。

训练过程中重点看两样东西:loss 曲线是否持续下探,mAP 是否在验证集上稳步上升。如果 loss 降到某一步不动了,不要反复加 epochs,先看是不是学习率设太高或者数据有问题。训练结束后模型存在runs/detect/train/weights/best.pt和last.pt,best.pt 是验证集上 mAP 最高的权重,last.pt 是最后一轮的权重。后续推理、验证、部署都用 best.pt 就对了。

如果习惯写 Python 脚本而不是敲命令,等价的方式是这样:

from ultralytics import YOLO model = YOLO("yolov8s.pt") model.train(data="traffic_light.yaml", epochs=150, imgsz=640, batch=16)

两种方式完全等价,训练参数都写在 train 函数的入参里。中途断了想接着训,用model.train(resume=True)即可。训练完别急着部署,先跑下一章的验证检查,不然模型表面指标好看,现场一用就露馅。

5. 红绿灯数据集训练避坑指南:五条血泪经验

5.1 标签类别号和 names 顺序不一致:mAP 曲线反常但 loss 正常

现象:训练过程 loss 一路下降,看着挺正常,但验证集 mAP 一直在低位徘徊,或者预测时红灯被识别成绿灯,路灯被识别成红灯。

原因:标签 txt 里的类别 ID 和 data.yaml 中的 names 顺序对不上。常见于两种情况:一是你改过 yaml 里的 names 顺序,二是划分脚本里硬编码的类别列表顺序和训练配置不一致。

解决:训练前先把标签里实际出现的类别 ID 统计出来,和 names 逐项对照。前面 2.2 节的统计脚本就能干这个活。发现不一致后,要么改 yaml 里的 names 顺序,要么把标签文件里的 ID 批量替换,不要手工人肉改,几百个 txt 会改到怀疑人生。我习惯的做法是写一个循环脚本把类别 ID 换掉,先打印前三个文件的修改前后对照,确认无误再全量跑。

5.2 图片带 EXIF 旋转信息导致标注框偏移

现象:图片在电脑上看是正的,LabelImg 里标注也对,但训练出来的模型检测框整体偏移,框住的位置偏上或偏下,尤其集中在某几类图片上。

原因:手机或部分相机拍出来的 JPG 带 EXIF 方向信息。LabelImg 显示图片时自动应用了方向校正,但保存的 xml 坐标是基于原始像素方向算的。YOLO 读取图片时直接用 OpenCV 解码,不处理 EXIF,两者方向不一致,框自然就错了。

解决:训练前把图片全部用 PIL 转正。代码非常短:

from PIL import Image, ImageOps import os for img_name in os.listdir("images"): path = os.path.join("images", img_name) img = Image.open(path) img = ImageOps.exif_transpose(img) img.save(path)

exif_transpose 会根据 EXIF 信息把图片像素真正转正,再保存覆盖原图。处理之前先备份原文件夹,这类操作不可逆,万一转出来色调变了还有后悔药可以吃。红绿灯数据集如果来源是监控抓拍,基本不会有这个问题,但只要你往里加过手机拍摄的图,就必须走这一步。

5.3 划分后小类别样本分布失衡:验证集虚高,现场拉胯

现象:验证集 mAP 高达 0.95,但是拿到真实路口测试,黄灯漏检严重,甚至一张图里三个灯漏了两个。

原因:划分脚本按图片维度随机切分,没有按类别做分层抽样。如果黄灯样本本身就少,又恰好大量落在训练集,验证集里黄灯少,模型只需要学会红灯绿灯就能拿高分,真实场景黄灯一出现就抓瞎。

解决:先跑 2.2 节的统计脚本,看看每类目标框数量。如果某类占比低于 10%,不建议纯随机划分,改为手动挑选包含该类目标的图片,按比例分配到训练集、验证集、测试集。常见做法是写脚本按类别索引图片,把包含黄灯的图片单独拎出来,先按 7:2:1 分好,再把其余图片随机填进三个集合。这类脚本一次性的,用 Python 写清楚逻辑跑一遍就行,别靠手在文件管理器里拖图片,几百个文件拖下来必出错。

5.4 显存溢出和 BN 崩溃:训练中断的两个高频原因

现象:训练到某个 epoch 突然报CUDA out of memory,进程直接终止;或者 loss 毫无征兆变成 nan,日志里跟着一串 RuntimeError,模型权重炸掉。

原因:显存溢出是 batch size 或 imgsz 设太大,显卡物理显存扛不住。BN 崩溃的原因比较复杂,常见的是数据里混入了全黑图片或损坏图片,导致 batch 统计量异常,也可能是学习率设得过高,梯度爆炸冲垮了 BN 层的统计参数。社区里把这个现象叫 BN 崩溃,论文里解决方案五花八门,工程上最有效的办法是先把脏数据揪出来删掉,再降学习率重训。

解决:显存问题把 batch 降到 4,imgsz 降到 416,或者开梯度累积,一句话就是让显存放得下。BN 崩溃时先跑一个数据完整性检查,把尺寸异常、全黑、全白的图片找出来:

find images -name "*.jpg" -size -5k

小于 5KB 的 jpg 基本都是坏的,删掉对应的标签文件再续训。续训用下面的命令直接跑:

yolo detect train data=traffic_light.yaml model=runs/detect/train/weights/last.pt epochs=150 imgsz=640 batch=16 resume=True

resume=True 表示从 last.pt 接着训,而不是从头开始。这是碰到意外中断后最常用的恢复手段,比重新跑一遍省几个小时。

5.5 用错划分脚本:测试集里混进了训练数据

现象:测试集 mAP 高得离谱,接近 0.99,换到没见过的摄像头视频上一测,性能直接崩盘,和测试集结果完全不在一个水平。

原因:把两份划分和三份划分脚本混着用了。比如先跑了三份划分,又跑了两份划分,输出目录里残留了旧文件,后一次划分没清空之前的数据,导致测试集里混入了训练集图片。或者直接把“训练集验证集划分脚本”当成了“训练集验证集测试集划分脚本”,压根就没生成真的测试集。

解决:划分前强制清空输出目录,划分后用一段小命令检查三个集合有没有重复文件名:

ls images/train images/val images/test | sort | uniq -d

uniq -d 会输出出现重复的图片名,只要有任何一行输出,说明集合之间有交叉,这次划分作废,清空重来。这个命令值得养成肌肉记忆,我每换一个数据集跑划分,必跑一次检查,十秒的事,能省后面几小时的返工。

6. 用 best.pt 做验证与推理:先把 mAP 放一边,用十张图自测

训练完拿到 best.pt,先别急着看 mAP 数字,那个指标太宏观,掩盖了太多细节。我会先跑一轮批量推理,用测试集做底,观察每张图的检测框位置和类别是不是符合直觉:

from ultralytics import YOLO import cv2 model = YOLO("runs/detect/train/weights/best.pt") results = model.predict("test_images", conf=0.25, save_txt=True, save_conf=True) for r in results: boxes = r.boxes print(len(boxes), "个目标")

conf=0.25 是置信度阈值,低于这个值的框会被过滤掉。红绿灯检测别一上来就拉高阈值,目标本身在画面里尺寸就小,置信度天然不会太高,拉到 0.5 会漏掉一半目标。先跑一遍看检出情况,再根据实际效果决定阈值放在哪。工程上我一般会存一批预测结果图,单独建个文件夹,逐张翻一遍。

mAP 的问题在于它是个全局统计量,验证集里红灯多绿灯少时,模型只需要把红灯学扎实就能拿高分,绿灯的错误被淹没在均值里。YOLOv8 训练完会在runs/detect/train下生成confusion_matrix.png,那个图比 mAP 曲线诚实得多,一眼就能看出红灯和绿灯之间是不是在互相混淆。如果混淆矩阵对角线很干净,再谈部署不迟。

我在真实项目里吃过亏:当时 mAP 刷到 0.96,觉得稳了,结果拿去跑一个偏傍晚的摄像头,黄灯几乎全漏,红灯框还上下抖动。后来养成一个习惯,每次训练完从测试集里挑十张有代表性的图:包括晴天逆光、夜晚、红灯绿灯同时出现的场景,打印出来一张一张对照标注和预测框。框中心有没有落在灯体上、类别标得对不对、有没有一个灯框出来三个重叠框,这些细节比任何指标都真实。从那以后我每次换数据集都强制走一遍这个十张图自测,再开始调参。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询