ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

XML/TXT格式转换指南:YOLO训练红绿灯与交通标志检测数据集

XML/TXT格式转换指南:YOLO训练红绿灯与交通标志检测数据集 简介面向计算机视觉目标检测实验与交通场景研究数据集中涵盖交通标志和信号灯两类核心目标由原创道路图片经labelimg人工标注整理而成适用于YOLO等主流目标检测模型的训练、验证与算法对比。资源共1641个文件包含877张png高清道路图片、761个xml标注文件、2个txt标签文件及1个python脚本压缩包整体约217.98MBxml保留原始矩形框坐标txt为转换后的YOLO格式标签写清类别编号与目标位置同时附带python脚本便于按需扩展处理与二次整理。样本覆盖限速牌、交通警告牌以及红灯、绿灯、黄灯等典型类别类别分布覆盖道路上的常见标志与信号灯适用于交通物流、智能驾驶等场景下的目标检测实验。已有475人学习下载适合需要直接取得已标注数据、快速进入模型训练或格式转换流程的开发者。1. 红绿灯检测数据集听着好找落地时全卡在格式上做目标检测实验的人十个里有八个第一反应是去翻开源数据集。交通标志和红绿灯这类场景公开数据确实不少但真正能把模型跑起来的人往往不是被模型难住的而是被标注文件的格式转换、类别 ID 映射、数据目录组织这些“看起来不值一提”的步骤卡死的。标题里同时出现 XML 和 TXT其实是把最关键的矛盾点摆出来了XML 是 PASCAL VOC 的标准标注格式TXT 是 YOLO 系列模型直接读取的归一化坐标格式一个数据集想喂给 YOLO 训练必须从前者转到后者或者直接拿到已经是后者格式的标注才能省事。这篇文章不是给你讲数据集有多好而是把“拿到 XML/TXT 格式的交通标志和红绿灯数据集之后到底怎么整理、怎么转换、怎么划分、怎么训练、会遇到什么坑”这条路完整走一遍。新手照做能跑通最小训练流程熟手也能在这里对照一下自己是不是踩过那些隐性的坑。内容全部基于 YOLOv5/v8 系列常规训练流程来写不涉及任何虚构的官方文档或仓库地址。2. XML 和 TXT 格式的本质区别PASCAL VOC 与 YOLO 的坐标系差异2.1 PASCAL VOC 的 XML 标注到底存了什么PASCAL VOC 格式的 XML 标注文件本质上是一个以图像文件名为前缀、与图像同名的独立文件。每个 XML 文件对应一张图片里面记录了图片的尺寸、通道数、以及图片里所有目标对象的类别名和边界框坐标。坐标的形式是xmin、ymin、xmax、ymax这四个值全部是像素绝对值直接对应图片宽高坐标系的像素位置。以一张 1280 x 720 的红绿灯图片为例图片里有一个红灯目标XML 里可能这样记录annotation foldertraffic_light/folder filenameimg_001.jpg/filename size width1280/width height720/height depth3/depth /size object namered_light/name bndbox xmin540/xmin ymin210/ymin xmax580/xmax ymax250/ymax /bndbox /object /annotation这个 XML 文件的核心就是size里的宽高和object里的类别名与坐标框。一个图片里有多少个目标就有多少个object块。name标签里的值就是类别名在 VOC 时代通常是car、person这类英文单词到了交通标志场景就是traffic_light、stop_sign、speed_limit_30之类的名字。理解 XML 格式的关键点在于坐标是像素值且图片尺寸必须在 XML 里记录下来。因为后续要转成 YOLO 的 TXT 格式必须用 XML 里的 width 和 height 对 xmin、ymin、xmax、ymax 做归一化。如果某些 XML 文件缺失size块或者尺寸记录和实际图片尺寸不一致转换后坐标就是错的模型训练时轻则指标异常重则直接报AssertionError: bbox is not correct。2.2 YOLO 的 TXT 标注为什么只有五个数字YOLO 系列的 TXT 标注文件格式极其简单每行一个目标共五个数字依次是类别 ID、归一化中心点 x、归一化中心点 y、归一化宽度 w、归一化高度 h。其中归一化是指所有值除以图片的宽或高所以 x、y、w、h 全部落在 0 到 1 之间。举个例子上面 XML 里那个红灯框转成 TXT 就是0 0.4375 0.3194 0.0312 0.0556第一列的0是类别 ID。这个 ID 不是随便编的它由数据集合根目录下data.yaml文件里的类别列表顺序决定。如果data.yaml里写的是names: 0: red_light 1: green_light 2: yellow_light 3: stop_sign那么red_light就是 0green_light就是 1。如果你的 XML 转换脚本里硬编码的 ID 和data.yaml的类别顺序对不上模型训练完预测框就全部错位而且从 loss 指标上完全看不出来。TXT 格式的一大优势是省空间、读取快。一个目标一行文本几个像素级别的数字YOLO 在训练时直接按行解析不需要像 XML 那样要先做 DOM 解析或遍历节点。另一层原因是 ultralytics 的 PyTorch 数据加载器在执行load_mosaic、随机仿射变换时需要直接操纵归一化坐标做矩阵变换这个计算在像素坐标系下做要反复换算归一化坐标下直接乘变换矩阵即可。2.3 XML 转 TXT 的转换脚本核心代码与三个必填参数这个转换脚本是整条数据准备链路里最值得认真写的部分。我常用的做法是一个 Python 脚本遍历一个标注目录里的所有 XML逐个解析然后将坐标换算后写入同名 TXT。核心代码大致如下import xml.etree.ElementTree as ET import os def convert_xml_to_txt(xml_path, txt_path, class_names): 将 PASCAL VOC XML 标注转换为 YOLO TXT 格式 tree ET.parse(xml_path) root tree.getroot() # 读取图片尺寸归一化必须依赖这两行 size root.find(size) width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: print(f警告: {xml_path} 中存在未定义的类别 {name}已跳过) continue cls_id class_names.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 转换为 YOLO 格式: 中心点坐标和宽高 x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines))这段脚本逻辑上有一个关键点class_names是一个列表列表里元素的下标就是最终 TXT 文件里的类别 ID。所以调用脚本时这个列表的传参顺序必须和后面data.yaml里的names顺序完全一致哪怕顺序不一样、名字一样也会导致 ID 错位。我一般把脚本封装成命令行调用传入三个必填参数XML 目录、TXT 输出目录、以及一个类别映射文件的路径。参数说明示例xml_dir存放 XML 标注的目录./labels_xmltxt_dir转换后 TXT 输出目录./labels_txtclasses_file类别列表文件每行一个类名classes.txt转换结束后一定要抽查几个文件。打开一张图片和它对应的 TXT目测框的位置是否贴合目标——这一眼检查比十行烂代码的约束检查都管用。2.4 图片与标注文件目录结构的标准组织方式YOLO 训练时对数据目录的约定比较常规图片和标注文件分别放在images和labels两个目录下且内部子目录结构完全一致。比如训练集图片放在images/train标注就放labels/train验证集图片放在images/val标注就放labels/val。看起来简单但实际用的时候经常出问题。有些数据集按场景分目录比如city_a、city_b每个目录下图片和 XML 混在一起有些数据集把 XML 统一放在根目录的Annotations下图片放在JPEGImages下——这个结构是 VOC 数据集的经典组织方式。你拿到的原始数据不管是什么结构第一步都应该先整理成 YOLO 期望的结构dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/整理目录时有个容易忽略的点图片和 TXT 文件必须同名同前缀。如果图片是img_001.jpg标注就必须是img_001.txt。任何多出来的后缀或前缀差异训练时都会报找不到标注文件的错误。YOLO 的训练逻辑是按图片路径去找对应 TXT 文件如果没找到训练阶段会跳过这张图但不会报错——最终表现就是训练集图片数量和标注数量对不上模型收敛异常。3. 从零跑通 YOLO 交通标志与红绿灯检测数据划分、YAML 配置、训练命令3.1 数据集划分需要避开的随机抽样陷阱拿到完整数据集之后第一步不是急着训练而是划分训练集和验证集。如果数据集本来就按要求整理成了images/train和images/val这一步可以跳过但很多公开数据集的划分方式存在问题常见的是直接按文件名随机抽 80% 做训练、20% 做验证。交通标志数据集有一个特殊的属性同一组红绿灯通常会在连续的几帧里反复出现而这些连续帧在文件名上往往只有序号差别。如果随机划分时不小心把同一场景的连续帧同时分进训练集和验证集验证集数据就会“泄漏”进训练集最终验证指标虚高部署到真实道路场景时准确率大幅下滑。更稳的做法是按视频片段或场景目录划分保证同一个场景的图片要么全在训练集要么全在验证集。划分脚本的常规做法是把所有图片列出按场景前缀分组然后对组集合做随机划分python split_dataset.py --image_dir ./images_all \ --label_dir ./labels_txt \ --train_ratio 0.8 \ --output_dir ./dataset划分完成后检查一下训练集和验证集的类别数量分布避免某个类别在验证集里只有个位数的样本。红灯、绿灯、黄灯这三类在自然场景中的比例天然不均衡——红灯和黄灯样本相对少如果验证集又随机抽掉了红灯的大量样本最终模型的红灯召回率会很差。3.2 写对 data.yaml类别顺序、路径写法、两个常见错误data.yaml是 YOLO 训练时的数据集配置文件它的内容不多但每一个字段都直接影响训练能否启动。一个交通标志和红绿灯混合检测的配置文件模板如下# 数据集根目录train 和 val 的路径都基于这个路径 path: /path/to/your/dataset # 训练集和验证集的图片目录 train: images/train val: images/val # 类别数量 nc: 4 # 类别名称顺序和转换脚本中的 class_names 必须一致 names: 0: red_light 1: green_light 2: yellow_light 3: stop_sign这个文件有三个坑。第一path必须是绝对路径或相对于当前工作目录的路径如果你在项目根目录下运行训练命令而path写的是相对路径YOLO 会按照当前工作目录去找找不到就报Dataset not found。第二names列表的顺序一旦确定就不能再改如果后来发现某个类别顺序错了改 YAML 之前必须重新跑 XML 转 TXT 脚本否则已有的 TXT 标注里类别 ID 全部错位。第三nc必须和names的长度一致写少了训练会忽略最后一个类别写多了训练时所有预测类别都会偏移一位。3.3 最小可运行的训练命令与参数含义数据准备好了YAML 写好了就可以启动训练。如果你用的是 ultralytics 系列框架训练命令非常简洁yolo train datadata.yaml modelyolov8n.pt epochs100 imgsz640 batch16 project./runs nametraffic_light_v1这行命令的核心参数是参数作用建议data指定数据集 YAML 路径必须是实际存在的路径model预训练权重或模型结构新手用yolov8n.pt追求精度用yolov8m.ptepochs训练轮数交通标志场景 100 轮基本足够imgsz训练图片缩放尺寸红绿灯目标小建议 640 起步batch批量大小显存不够就调小8GB 显卡用 16 比较稳这里重点说一下imgsz的选择。红绿灯在 1280 x 720 的高清画面里往往只占几十个像素直接缩放到 640 会丢失很多细节。职业做法是先用 640 跑基础训练然后解冻模型做 1280 的微调——也就是把imgsz调大用较小的学习率继续训练几十轮。这个过程能显著提升小目标召回率代价是显存占用变大、训练速度变慢。3.4 训练完的产物weights 文件、混淆矩阵、PR 曲线怎么读训练完成后Ultralytics 会在project/name目录下生成一组文件。weights/best.pt是验证集上指标最优的权重weights/last.pt是最后一轮的权重实际部署时优先用best.pt。confusion_matrix.png展示每个类别的预测混淆情况红绿灯场景重点看红灯和绿灯是否互相混淆——如果是说明模型在颜色特征上的区分度不足需要补充更多夜间或背光场景的数据。PR_curve.png是精确率和召回率的曲线曲线下面积越大越好如果某类别的曲线掉得很早说明置信度阈值低时误检率高。跑验证集可以用yolo val modelruns/traffic_light_v1/weights/best.pt datadata.yaml验证输出里最应该关注的是每个类别的mAP50-95。交通标志和红绿灯这类大目标居中场景mAP50 到 0.9 以上通常不难但 mAP50-95 更能反映定位精度——红绿灯框如果偏半个灯体mAP50-95 就会掉很多而 mAP50 可能看着没变化。4. 三个必调的检测参数置信度阈值、IOU 阈值、图像缩放策略4.1 confidence threshold 调低还是调高取决于你的场景容错训练完成后做推理默认的置信度阈值是 0.25。在交通标志场景里这个值通常偏高。红绿灯目标小、纹理少、有时还带模糊运动模型输出的置信度天然不高。如果按默认阈值跑很多真实的红绿灯会被直接过滤掉看起来像是模型没检测到。实际处理时分两个场景。如果是离线分析视频或图片我一般把置信度阈值降到 0.1 左右这一步可以捞回大量低置信度但坐标准确的框后续可以用跟踪或时序投票来消除误检。如果是部署在实时检测系统里置信度阈值保持在 0.3 到 0.4 更合适宁可漏检也不能频繁误报——红绿灯检测的误报比漏报更危险它可能让车载系统把红灯看成绿灯或相反。在代码里调阈值from ultralytics import YOLO model YOLO(best.pt) results model.predict(test_img.jpg, conf0.1, iou0.5)conf参数就是置信度阈值iou是 NMS 的 IoU 阈值。如果你发现同一个红绿灯周围出现多个重叠框把iou调低到 0.3重叠框会被更激进地抑制掉如果发现相邻的两个信号灯被合并成了一个框把iou调高到 0.7保留更多候选框再做筛选。4.2 图像缩放策略letterbox 和直接 resize 的区别YOLO 训练和推理时默认采用 letterbox 缩放也就是把原始图片按比例缩放到目标尺寸不足的部分用灰色填充。这种做法的好处是目标不会因为拉伸而变形对交通标志这种尺寸比例敏感的目标至关重要。如果你为了省事直接调用 OpenCV 的cv2.resize做强制拉伸红绿灯圆形灯体会被拉成椭圆模型精度下降是必然的。推理阶段手动处理时要注意保持 letterbox 一致。如果用model.predict接口框架内部已经处理了 letterbox不需要自己操作。但如果用 ONNX 或 TensorRT 部署需要自己实现 letterbox 预处理并且推理出框后还要把坐标从 letterbox 后的坐标系映射回原图坐标系——这一步是部署时最常见的坐标错位来源。映射公式是# 把 letterbox 后的坐标映射回原图坐标 scale max(orig_w / new_w, orig_h / new_h) pad_x (new_w - orig_w * scale) / 2 pad_y (new_h - orig_h * scale) / 2 x_center_orig (x_center_new - pad_x) / scale y_center_orig (y_center_new - pad_y) / scale w_orig w_new / scale h_orig h_new / scale4.3 在 ROS 或 OpenCV 推理脚本里实际调参的顺序很多红绿灯检测项目最终不是在 Jupyter Notebook 里跑的而是嵌入到 OpenCV 或 ROS 的视频流处理管线里。在连续帧场景下调参顺序是先把置信度调到最低0.05让模型把所有疑似目标全输出然后看是否有明显误检接着逐步提高阈值观察误检和漏检的权衡曲线最后调 IOU 阈值消除可能存在的密集重叠框。这个顺序能帮你快速定位一个检测失效的问题到底是模型的锅还是后处理的锅。5. 避坑指南XML/TXT 数据集落地时的 5 个高频报错与处理记录5.1 现象训练时提示AssertionError: bbox is not correct训练刚开始数据集校验阶段报这个错指向某张图片的标注框坐标异常。最常见原因是 XML 转 TXT 时坐标计算错误xmin 大于 xmax或者 ymin 大于 ymax极少数情况是某个坐标值是负数或超过了图片宽高。解决方式写一个简单的扫描脚本遍历所有 TXT 文件里的每一行检查五个数字是否满足基本约束——类别 ID 小于类别总数x、y 在 0 到 1 之间w、h 在 0 到 1 之间且不为 0。import os def check_label_file(txt_path, num_classes): with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f格式错误: {txt_path} - {line}) return False cls_id, xc, yc, w, h map(float, parts) if cls_id num_classes or xc 0 or xc 1 or w 0 or w 1: print(f数值越界: {txt_path} - {line}) return False return True出现负坐标还有个常见原因标注工具里有的框没裁剪到图片内部跑出画面了。这类框在转换时直接 clip 到 0 到 width/height 之间就行。5.2 现象训练集 loss 下降正常验证集 mAP 长期在低位徘徊这是典型的过拟合或数据划分泄漏问题。交通标志数据集如果同一场景的连续帧同时出现在训练和验证里模型会记住场景纹理而不是学习信号灯特征导致验证集 mAP 虚高。但如果验证集划分得太“偏”——比如恰好把大量夜间数据都分到验证集而训练集全是白天数据模型在验证集上表现差就是必然的。解决方式回到 3.1 节按场景分组划分。如果数据集没有场景编号可以用文件名前缀做分组。另外检查数据增强策略——YOLO 默认的hsv_h、hsv_s等增强参数如果调得过大对红绿灯这种颜色强相关目标反而有害红灯被增强成粉色模型自然学偏。我一般把hsv_h从默认的 0.015 降到 0.005。5.3 现象predict 时输出的类别名全部错位一位模型在验证集上指标正常但拿一张只有 stop sign 的图片去测试输出的类别却是 green_light 或类似的其他类。这个问题的根源是data.yaml里的names顺序与训练时的 TXT 标注类别 ID 对不上。最常见的情况是训练完之后有人为了测试某个类别改名或调整了 YAML 顺序但没有重新转换标注文件。解决方式在训练完成后立刻把data.yaml记录到权重文件所在目录部署推理时保持完全一致。任何类别的增删改都必须同时更新 TXT 标注和 YAML 文件。这个环节可以说是所有目标检测项目里最容易翻车的地方尤其在多人协作时——一个人改了 classes.txt另一个人按旧顺序重新转换了一批 XML。5.4 现象用自己的图片测试小目标红绿灯完全检测不到模型在验证集上 mAP 不错但实测时远处的小红绿灯一个都看不见。这个问题的原因通常是训练时的imgsz太小。红绿灯在 640 分辨率的输入图上可能只有 6x6 像素这个尺寸下信息量严重不足。解决方式把训练imgsz提到 1280或者对图像做切片训练——将原图按 2x2 切成四块每块单独训练和预测最后把检测结果映射回原图坐标。切片训练在交通标志场景里非常有效但要注意切片时标注文件里的坐标也要跟着切——切到边界上的目标会被截断这类目标的标注需要做边界裁剪处理。5.5 现象模型把红灯和绿灯混淆颜色特征学不出来红绿灯和交通标志在颜色上有强区分度但实际数据里有很多特殊情况背光环境下绿灯泛白、红灯在白天的强曝光下变成橙色、黄灯在雾天几乎无法辨识。如果训练集里这类困难样本太少模型就容易只靠位置特征来判断——比如只检测灯头而忽略灯的颜色进而把红灯和绿灯混到一起。解决方式数据增强里加大饱和度扰动幅度hsv_s从默认 0.7 调到 1.0。如果条件允许补充夜间、逆光、雨雾天气的样本这是最治本的方法。调参只能解近渴数据是唯一能真正提升颜色区分度的路径。6. 模型验证的进阶习惯用真实视频做回放式自测训练结束、模型保存好这只是开始。我会用一个自己长期保持的习惯做最终验收找一段没出现在训练集里的真实街道视频——包含白天、夜晚、顺光、逆光、雨天五种情况用训练好的模型逐帧推理把每一帧检测到的红绿灯坐标和类别写成一个文本文件。然后对这个结果做两个层面的分析。第一层是逐帧覆盖度统计有多少帧里出现了未被检测到的红绿灯。红绿灯在视频里是持续显示的如果某一帧检测不到相邻帧通常能检测到所以单帧漏检不是致命问题但如果你发现某个路口的红灯连续 10 帧以上都没有被检测到这个方向就是场景级的失败需要补充该场景的数据重新训练。第二层是时序稳定性分析读取输出的坐标序列检查同一个红绿灯的检测框坐标是否在相邻帧之间发生跳变——如果前一帧的 x 坐标是 100下一帧直接跳到 240说明模型对目标位置的定位不稳定通常由模糊帧或目标过小导致。实测时我会写一个非常轻量的脚本做这个检查import json with open(frame_detections.json, r) as f: detections json.load(f) for track_id, frames in detections.items(): coords [frame[x_center] for frame in frames] max_jump max(abs(coords[i] - coords[i-1]) for i in range(1, len(coords))) if max_jump 50: # 单位: 原图像素 print(f轨迹 {track_id} 存在坐标跳变: {max_jump}px)这里的max_jump阈值需要根据实际视频分辨率和红绿灯大小来定。在 1280x720 的画面里一个正常稳定的检测框在相邻帧之间的偏移通常不超过 20 像素如果超过 50 像素就值得检查一下那一帧是不是运动模糊或曝光变化过大。这个验证习惯是从自动驾驶视觉系统项目里学来的——单张图片上的指标再漂亮也不如一段真实场景视频的回放更有说服力。红绿灯检测做到后面拼的不是模型的层数而是你对数据分布的理解和对验证方法的较真程度。希望这个思路能帮你在自己的数据集上少走一段弯路。本文还有配套的精品资源点击获取
返回列表