
简介面向目标检测入门与交通场景实验的YOLO格式交通标志与信号灯数据集涵盖红绿灯、限速牌、警告牌等多类目标已使用LabelImg完成标注并转换为YOLO所需的txt坐标格式适合高校课程设计、毕业设计及算法对比实验。压缩包共1641个文件主要由877张PNG高清图片、761个XML标注文件、2个TXT类别说明及1个Python转换脚本组成整体约218MB图片与标注文件按道路场景编号保存便于划分训练集与验证集。目前已有475人学习下载可直接用于PP-YOLO、YOLOv5、YOLOv8等主流检测模型的训练与验证。资源内含坐标转换脚本与标注格式说明可帮助用户理解xml与txt格式的映射关系免去手工整理的繁琐步骤快速起步实验。1. 拿到数据集先别急着训练这份 YOLO 交通标志红绿灯检测数据集的 xml 和 txt 到底怎么用解压一份交通标志和红绿灯检测数据集最常见的困惑是里面同时躺着 Annotations 目录的 xml 和 labels 目录的 txt两套标注看着重复不知道该以哪套为准去训练 YOLO。实际做目标检测实验时xml 是 VOC 系标注工具直接导出的结果txt 是 YOLO 系训练真正读取的格式两者描述的是同一批框只是坐标系和存储方式不同。这篇笔记从这两种格式的区别讲起给出一套能直接复制的 xml 转 txt 脚本、训练前的核查清单以及红绿灯这类小目标场景特有的踩坑记录适合正在做课程设计、毕设或论文对比实验的从业者照着走一遍。2. 两种标注格式的来龙去脉xml 和 txt 分别代表什么、为什么数据集要留两份2.1 VOC 系的 xml绝对像素坐标和它的字段习惯目标检测领域里xml 标注最常指的是 PASCAL VOC 格式。用 LabelImg 这类目标检测常用标注工具画完框默认保存的就是 xml。打开一个典型的交通标志标注文件结构大致是这样annotation folderJPEGImages/folder filenamestop_sign_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namestop/name bndbox xmin412/xmin ymin635/ymin xmax768/xmax ymax891/ymax /bndbox /object /annotationxml 文件怎么打开和编辑新手往往卡在第一步。常见的做法是用 VS Code 或记事本直接打开看内容装一个 XML Tools 插件可以让节点折叠、高亮更清楚。不要用 Word 打开它会尝试渲染并可能悄悄改格式。xml 解析在代码里通常基于 xml.etree.ElementTree 或 lxml后者的 xpath 能力更强但只做读取和转换的话标准库就够用。要特别注意的是 bndbox 里的四个值全都是像素绝对坐标xmin 和 ymin 是框左上角的横纵坐标xmax 和 ymax 是右下角。坐标系原点在图片左上角x 向右增大y 向下增大这跟很多人习惯的数学坐标系相反写转换脚本时容易把高和宽搞反后面会专门说这个坑。2.2 YOLO 系的 txt归一化坐标和每行一个目标YOLO 系列训练时读取的标注是纯文本 txt每一行对应一个目标五个数字依次是类别编号、归一化中心点 x、归一化中心点 y、归一化宽度 w、归一化高度 h。拿上面那个 stop 标志举例假设图片宽 1920、高 1080中心点 x (412 768) / 2 / 1920 ≈ 0.3073中心点 y (635 891) / 2 / 1080 ≈ 0.7065宽度 w (768 - 412) / 1920 ≈ 0.1854高度 h (891 - 635) / 1080 ≈ 0.2370对应 txt 行就是0 0.3073 0.7065 0.1854 0.2370。如果类别编号是 0代表这个目标属于类别表里的第一类。txt 格式有几个隐含约定。第一所有坐标值都在 0 到 1 之间所以不管输入图片是 640×640 还是 1280×1280YOLO 训练时把图片 resize 成任意尺寸标注都不需要重新算。第二txt 文件名必须和图片文件名完全一致只是后缀不同YOLO 靠这个同名关系去找标注不会去解析 txt 内部有没有文件名。第三一行一个目标目标多了 txt 就有多行没有目标时文件是空的这个空文件本身也是有效标注代表这张图没有要检测的东西。2.3 两张表看懂 xml 和 txt 的字段对应关系把 VOC 的 xml 和 YOLO 的 txt 字段放在一起对照转换时不容易漏字段xml 字段txt 位置含义换算关系object/name第 1 个数字类别类别名称通过类别表映射为整数编号bndbox/xmin无框左边界像素坐标用于计算 cxbndbox/ymin无框上边界像素坐标用于计算 cybndbox/xmax无框右边界像素坐标用于计算 wbndbox/ymax无框下边界像素坐标用于计算 hsize/width无图片像素宽归一化分母size/height无图片像素高归一化分母object/difficult无难例标记转换时通常丢弃YOLO txt 没有直接保存 difficult 或 truncated 这些扩展属性这是格式本身的表达能力决定的。VOC 里标为 difficult 的目标一般建议直接丢弃或转成一个特殊类别不要强行塞进 YOLO 训练否则模型会困惑这个框到底要不要学。2.4 数据集为什么要同时保留 xml 和 txt以及交通标志红绿灯的特殊性一份正规的检测数据集同时放 xml 和 txt最直接的原因是面向不同框架的用户。YOLO 系训练只认 txtMMDetection 和 Detectron2 这类框架有自己的中间格式但很多人的流程是先要 VOC 的 xml 再转成自己需要的结构。发布者把 xml 留一份相当于给用户留了后悔药txt 丢了可以从 xml 重新生成txt 的类别序号乱了也可以回到 xml 重新映射xml 再丢了就只能重新标注成本完全不是一个量级。交通标志和红绿灯这份数据和普通物体检测数据集有明显区别。红绿灯的灯体本身很小在 1920×1080 的图里可能只占 30×60 像素折算到 640×640 的训练分辨率下连 20×20 像素都不到属于典型的小目标。而交通标志的尺寸分布就很分散远处的禁令标志可能只有十几个像素近处的指示牌能占到画面四分之一。这两种物体混在同一个数据集里类别数不多但尺度跨度极大训练时对小目标的召回往往很差后面会专门讲怎么处理。3. 把 xml 转成 YOLO 能用的 txt可复制的转换脚本与验收清单3.1 转换前的目录整理三个文件夹先摆好拿到数据集后第一步不是写脚本而是把目录结构理清楚。YOLO 训练时约定 images 放图片、labels 放 txt两个目录下文件名一一对应。我一般会在数据集根目录下建这样一套结构mkdir -p datasets/traffic/images datasets/traffic/labels datasets/traffic/Annotations把 jpg 或 png 图片统一拷到 images把原始 xml 拷到 Annotationslabels 目录留空等待转换脚本写入。这样做的目的是把原始标注和生产标注分开后续不管是重新转换还是手工修复 xml都不会污染已经生成好的 txt。这里有一个很多新手会忽略的细节图片文件的扩展名最好统一。YOLO 的 dataloader 在匹配图片和标注时只认前缀名如果一张是.jpg一张是.jpeg文件名前缀相同但扩展名不同部分版本的工具链会匹配失败表现为训练时突然少了一批图。统一扩展名再进入下一步能省掉很多排查时间。3.2 核心转换脚本一个函数处理单个 xml下面这段是一份完整可跑的转换脚本用 Python 标准库实现不需要额外安装依赖。它读取一个 xml 文件解析出所有目标框按类别映射表转成 YOLO 格式的归一化坐标写入同名 txt。import xml.etree.ElementTree as ET import os # 类别表手工指定顺序就是训练时的类别编号 CLASS_MAPPING { red: 0, green: 1, yellow: 2, off: 3, stop: 4, speedlimit: 5, } def convert_xml_to_txt(xml_path, txt_path, class_mapping): tree ET.parse(xml_path) root tree.getroot() # 优先使用 xml 里的 size但实际项目中最好以真实图片尺寸为准 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_mapping: print(f[skip] unknown class {name} in {xml_path}) continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 越界保护VOC 坐标偶尔会有一两个像素出图 xmin max(0.0, min(xmin, img_w)) ymin max(0.0, min(ymin, img_h)) xmax max(0.0, min(xmax, img_w)) ymax max(0.0, min(ymax, img_h)) # 过滤掉无效框宽或高为 0 的标注没有意义 if xmax xmin or ymax ymin: print(f[warn] invalid box in {xml_path}) continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h cls_id class_mapping[name] lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) # 调用示例把一个 Annotations 目录下所有 xml 全量转换 if __name__ __main__: ann_dir datasets/traffic/Annotations label_dir datasets/traffic/labels os.makedirs(label_dir, exist_okTrue) for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue base os.path.splitext(xml_file)[0] convert_xml_to_txt( os.path.join(ann_dir, xml_file), os.path.join(label_dir, base .txt), CLASS_MAPPING, ) print(conversion done)这段脚本的每个参数都可以按你的数据集改。CLASS_MAPPING 是核心中的核心它决定了类别名称到编号的映射一旦确定了就不能中途乱改否则训练好的模型权重和这个映射对不上推理时全部类别错位。脚本里的越界保护用 min/max 把坐标钳制到图片范围内是因为部分 xml 里手滑标注出了几个像素的边界误差不处理的话转出来的 w 或 h 会大于 1YOLO 训练时直接报坐标异常。最后的 cx/cy/w/h 保留 6 位小数足够用再多反而让 txt 文件变大没有实际精度收益。值得强调的一个细节是脚本默认信任 xml 里的 size 字段做归一化分母。实际项目中 xml 的 size 偶尔会和真实图片不符比如标注工具版本问题导致 size 写的是占位值。更稳妥的做法是在转换前用 OpenCV 读一遍真实图片的高宽用它代替 xml 里的 size。如果数据量很大不想每张都读图至少要抽查一部分确认 size 字段和图片一致再批量跑。3.3 类别表怎么定先扫描所有 xml 再决定映射很多人转换时会犯一个错拍脑袋先写死类别映射结果扫描 xml 时发现还有没见过的类名脚本打了一堆 skip 就直接跑完了这些没映射上的目标全部被丢弃训练集莫名其妙缩水。我一般会先把整个 Annotations 目录里出现过的类名统计一遍再做映射。在转换脚本前面加一段统计逻辑from collections import Counter def scan_classes(ann_dir): counter Counter() for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, xml_file)) for obj in tree.getroot().iter(object): counter[obj.find(name).text] 1 return counter # 输出形如 {red: 1520, green: 1310, stop: 890, ...}拿到统计结果后再按频率或者按语义确定类别顺序。常见的做法是优先保证同一类交通标志和同一类灯色编号连续便于后面看混淆矩阵时好定位。类别表的顺序对模型精度没有本质影响但会影响你后续读日志和可视化的体验所以一开始排好序后面会省心很多。这里还要提一个关于类别粒度的决策点。红绿灯数据集的类别可以按red / green / yellow / off分也可以按red_left / green_straight这种带方向的分交通标志可以按stop / speedlimit / yield直接分也可以先分成禁令 / 警告 / 指示再细分。类别太少模型容易混淆比如把红灯的箭头方向搞错类别太多又会导致单类样本量不足。一般课程设计和实验用途灯色分 4 类、标志按具体语义分 10 到 20 类是够用的不要盲目堆细粒度。3.4 转换后必须做的三件事数量核对、格式抽查、可视化画框转换完不能直接开训练先做三项验收。第一步核对数量统计每个 xml 里的 object 数量和对应 txt 的行数是否一致。最简单的办法是写一个对比脚本import os ann_dir datasets/traffic/Annotations label_dir datasets/traffic/labels mismatch [] for xml_file in os.listdir(ann_dir): base os.path.splitext(xml_file)[0] txt_file os.path.join(label_dir, base .txt) import xml.etree.ElementTree as ET tree ET.parse(os.path.join(ann_dir, xml_file)) obj_count sum(1 for _ in tree.getroot().iter(object)) if not os.path.exists(txt_file): mismatch.append((base, obj_count, -1)) continue with open(txt_file, r) as f: line_count len([l for l in f.read().splitlines() if l.strip()]) if line_count ! obj_count: mismatch.append((base, obj_count, line_count)) if mismatch: for m in mismatch: print(fxml objects{m[1]} txt lines{m[2]} file{m[0]}) else: print(all matched)这个数量核对的逻辑很直白正常情况下每个 object 都应该转成一行 txt数量对不上就说明有类名没映射上、有框被过滤或文件没写进去。第二步是格式抽查随机打开几个 txt看每一行是不是 5 个数字且所有坐标值都在 0 到 1 之间。可以用一段简单的 Python 做校验如果发现某一行有 4 个数字或者大于 1 的坐标值多半是源 xml 的 size 字段有问题。第三步也是最有价值的一步可视化画框验证。用 OpenCV 把标注框画回原图人眼扫一遍确认框的位置、大小和类别与真实物体吻合。这一步能发现很多数字层面看不出的问题比如框整体偏移半个身位、类别标错、红绿灯只框了灯体没框灯杆等。我建议每个类别至少抽 20 张图做可视化覆盖白天、夜晚、背光三种光照条件别只挑质量好的图看。3.5 训练集的划分和 data.yaml 生成train.txt / val.txt 与类别配置YOLO 训练时会读取一个 data.yaml 文件里面写清楚训练集和验证集的图片路径、类别数量 nc 和类别名称 names。图片列表文件 train.txt 和 val.txt 每行是一个图片的绝对路径或相对路径YOLO 会根据图片路径自动在同目录找同名 txt 标注。# 划分训练集和验证集按 8:2 比例随机切分 find datasets/traffic/images -name *.jpg | shuf all.txt head -n 800 all.txt datasets/traffic/train.txt tail -n 801 all.txt datasets/traffic/val.txt这里有一个容易踩的坑shuf是随机打乱但如果数据集本身按路段分组同一条路段的照片会被打散到训练集和验证集两边导致验证集指标虚高这个问题第 4 章会详细讲。更严谨的划分方式是按视频片段或拍摄时间分组确保同一场景只出现在一边。data.yaml 的内容也顺手给出来path: datasets/traffic train: train.txt val: val.txt nc: 6 names: [red, green, yellow, off, stop, speedlimit]路径这块注意两点第一path建议用相对路径如果数据集被别人拷走换了一台机器只要目录结构不变就能直接跑用绝对路径的话路径里有中文或空格时容易出现编码问题。第二names的排列顺序必须和转换脚本里的 CLASS_MAPPING 完全一致顺序不一致时 YOLO 训练不会报错但模型输出的类别含义就全错了这种错位是最难排查的。4. 交通标志红绿灯检测训练避坑六个最容易翻车的地方4.1 类别名和序号对不上loss 看着正常但预测类别全错现象训练结束推理时红灯被识别成绿灯限速标志被识别成停止标志单看每个框的位置都准就是类别错得离谱。原因转换脚本用的 CLASS_MAPPING 和 data.yaml 里的 names 顺序不一致。比如 xml 里red映射成 0但 data.yaml 里第一个名字写的是green模型学到的 0 号类别对应的是红色灯体推理时却被解析成绿色。解决训练前用一段脚本从 Annotations 扫描类名按字母序或频率生成映射然后同步写入 data.yaml。或者更简单只用一套映射来源比如把 CLASS_MAPPING 的 key 按顺序输出成 data.yaml 的 names两个文件由同一个脚本生成从源头杜绝不一致。4.2 灯色状态标注的语义冲突同一个灯体被标成不同类别导致训练震荡现象训练集里红灯和绿灯的标注框位置高度重叠因为同一个物理灯体在不同照片里亮不同颜色。模型训练时这两类的 loss 一直降不下去验证集上红灯绿灯互相误判。原因红绿灯的颜色是状态而不是独立的物理实例。同一组灯体在红灯照片里标成 red在绿灯照片里标成 green模型看到的输入特征几乎一样但标签不同相当于让同一个位置学会两种矛盾的输出。解决一种做法是把它当成多状态分类问题在结构上让灯体检测和颜色分类解耦先检测灯体再分类颜色但 YOLO 单阶段结构不方便直接做这件事。实际项目里更常见的做法是接受这种重叠标注但要控制同灯体的不同状态不要同时出现在同一张图里且保证各类别样本均衡。如果数据集里出现同一张图同一个灯体既有红灯框又有绿灯框的标注那基本上可以判定标注质量有问题优先清洗数据而不是调参。4.3 小目标漏检红绿灯在 640 分辨率下只有十几个像素现象验证集 mAP 看着还行一放到实拍视频里远处的红绿灯基本全漏只有靠近停车线的大灯才能检出来。原因红绿灯和远处的交通标志天然是中小目标。以 1920×1080 原图里一个 40×60 像素的灯体为例缩放到 640×640 训练分辨率后只剩约 13×20 像素而 YOLO 默认配置的 anchor 偏向中等尺寸物体小目标很容易被忽略。红绿灯的漏检又尤其隐蔽因为验证集里小目标占比不高时mAP 会被大目标拉上去。解决推理时用 1280 输入尺寸而不是 640这是性价比最高的手段。训练时把 img 也调到 1280 会让显存消耗翻好几倍V100 这类卡能扛普通消费卡建议先用 640 训练再用 1280 做测试时增强。还有一个常用技巧是检测前做切片把原图切块后分别检测再合并结果但会增加推理耗时。最根本的验证方式是单独统计小目标区间的 AP而不是只看整体 mAP。4.4 xml 里的 size 和真实图片不一致画框偏移和坐标越界现象转换后可视化发现一部分图片的框整体向右下方偏移框的大小也跟着不对有些框甚至超出了图片边界。原因标注时用的图片尺寸和最终发布的数据集图片尺寸不一致。比如标注时看的是 1920×1080 的截图发布时被压缩成 1280×720xml 里的 size 字段却没更新。转换脚本用 xml 的 size 做归一化分母算出来的是相对于 1920×1080 的归一化坐标训练时却用在 1280×720 的图上转换关系全错。解决转换前用 OpenCV 读取真实图片尺寸覆盖掉 xml 里的 size再按真实尺寸归一化。这个错误在纯数字层面很难发现因为坐标值都还在 0 到 1 之间只有可视化或者对框比对图片分辨率时才暴露。经验是拿到任何数据集先随机抽 5 张图比对 xml 的 size 和实际图片分辨率不一致的比例一旦超过一成就必须全量改成读图取尺寸。4.5 空标注文件0 字节 txt 不报错但会悄悄污染训练现象训练日志里偶尔出现 No labels found in... 的 warning训练没中断但 loss 曲线出现不明原因的抖动。原因图片本身没有目标xml 里没有 object转换后生成 0 字节的 txt这种情况是正常的YOLO 会把它当负样本处理。但如果 xml 转 txt 时因为类名没映射上而跳过了所有目标同一个文件也会生成空 txt这时它其实应该有标注却没标出来模型会把有目标的图当负样本学学习信号被污染。解决区分两种空文件。正常负样本是图片里确实没有交通标志和红绿灯异常空文件是 xml 里有 object 但全被跳过。前面第 3 章的数量核对脚本能直接查出第二种情况统计出现xml objects0 且 txt lines0的文件逐个检查类别名是不是漏进映射表了。正常负样本建议在数据集里保留 5% 到 10%完全不掺负样本会让模型在背景区域乱框全负样本又会让训练发散。4.6 数据集划分泄漏验证集指标虚高实拍就露馅现象训练损失和验证 mAP 都很漂亮模型放到行车记录仪视频里效果断崖式下跌。原因划分训练集和验证集时用了完全随机的方式同一条路段连续拍摄的帧被打散到两边。验证集里出现的画面和训练集高度相似模型记住了场景而不是学到了泛化特征。红绿灯数据集尤其容易犯这个错因为红绿灯的位置固定、拍摄角度固定不同时间拍的同一路口几乎一模一样。解决按拍摄路段、视频片段或时间戳分组做划分。如果数据集没有提供这些元信息至少按文件名前缀分组比如文件名是按 clip01_frame001 这种格式命名的就以 clip01 为单位分配数据。划分完后做一个抽查从验证集里随机挑几张图看训练集里有没有同路段的其他帧有就说明泄漏了。这一步是红绿灯检测项目里最值得花时间的环节划分质量直接决定你评估出来的指标有没有意义。5. 训练前的一次体检跑 V100 之前先做这三处核查很多人拿到数据集直接就开始训练把训练过程当黑匣子跑完看眼 mAP 就宣布完事。我的习惯是投入显卡资源之前先花二十分钟做一次体检血泪经验告诉我这一步能省掉好几轮返工。第一处核查图片和标注是否完全配对。用一段命令统计两边文件数差值一定要为 0。我常用ls datasets/traffic/images | wc -l和ls datasets/traffic/labels | wc -l对比数字不一致就回头查是哪几张图少了 txt而不是直接开训练。第二处核查data.yaml 里的 nc 跟实际类别数量是否一致names和转换脚本的 CLASS_MAPPING 是否同序。把两个文件并排看一眼最多五分钟的事但能避免训练几十小时后发现所有类别标签都错位的灾难。第三处核查先跑一个 10 epoch 的短训练只看 loss 曲线和第一张验证集可视化输出确认 loss 在稳定下降、画框位置没有系统性偏移再上完整训练。V100 这类卡虽然快但完整训练也要几小时不值得拿它试探配置对错。还有一个针对红绿灯任务的验证技巧训练完后单独看混淆矩阵里 red、green、yellow 三类之间的互混情况。红绿灯场景里off灭灯和red之间的混淆也很常见因为背光或夜间条件下红色灯体和灭灯灯体的外观差异极小。如果混淆矩阵里这两个类别互相串得厉害优先检查训练集里这两类的光照条件是否均衡而不是急着改网络结构。说到底数据集的质量决定了模型的天花板损失函数和超参只是在这个天花板下面做微调。把格式转换、类别映射、数据划分这三件事做扎实比调任何参数都更值得投入。我的做法一直是从标注文件反推数据集的真实分布先让脚本把统计信息全部打出来再决定怎么训练。希望帮到你也欢迎你按这套流程跑一遍看看自己手里那份 xml 和 txt 到底藏了多少雷。本文还有配套的精品资源点击获取