ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLO交通标志与红绿灯检测:xml转txt格式及训练实战

YOLO交通标志与红绿灯检测:xml转txt格式及训练实战 简介面向目标检测实验的交通标志与交通信号灯数据集涵盖限速牌、交通警告牌及红绿灯中的红灯、绿灯、黄灯等常见类别适合自动驾驶、智能交通场景下的YOLO系列模型训练与验证也可供目标检测初学者学习标注规范。压缩包内共1641个文件核心包括877张高清PNG图像、761个经LabelImg标注的XML文件、2个TXT标签文件及1个Python脚本整体约217.98MBXML保留原始标注框位置TXT已转换为YOLO所需格式Python脚本可用于辅助数据划分或格式转换。数据由作者原创采集并标注场景和光照条件较为多样能提升模型在实际道路环境中的泛化能力两类标注文件并存查看细节与接入训练流程都很方便。目前已有475人学习浏览适合正在开展交通场景目标检测实验、毕业设计或算法测评的读者使用。1. 先搞懂这份“交通标志红绿灯”检测数据集的真实价值做目标检测实验的人尤其是刚拿到 YOLO 系列准备训练自己第一个模型的初学者大概率会碰到同一个尴尬公开数据集要么是 COCO 这种通用场景里面根本没有专门的交通标志和红绿灯类别要么是某篇论文的官方数据格式是 VOC 的 xml还得自己写脚本转成 YOLO 要的 txt。这份标题里提到的“YOLO 交通标志 交通信号灯 红绿灯 检测数据集 xml txt格式”解决的就是这个“想训练交通场景检测器却卡在数据和格式上”的典型问题。它的核心价值不在于数据量有多大而在于它同时提供了两种格式xml 对应 PASCAL VOC 标注体系txt 对应 YOLO 的归一化坐标体系。这意味着你可以直接用这份数据跑通“xml → txt”的转换流程也可以跳过转换直接用 txt 训练 YOLOv5/v8/v11。对于做毕设、课程实验、或者刚入门目标检测的开发者来说这种“到手就能用”的属性比动辄几十 GB 的完整数据集更友好。你不需要去爬图、不需要自己用 LabelImg 重新标注省掉的是整个项目里最枯燥、最耗时的一步。当然它也不是万能的。交通标志和红绿灯检测在真实场景里极其依赖地域性——欧洲的圆形限速牌、中国的倒三角减速标志、美国的黄色菱形警告牌类别定义完全不同。这份数据集具体覆盖了哪些标志类别你需要拿到文件后先看一眼类别清单再决定它是否适配你的任务。接下来我会从格式选型、转换脚本、训练验证、常见坑位这几个维度把这条路上能踩的坑都提前标出来。2. VOC 的 xml 与 YOLO 的 txt两种标注格式的选型逻辑与核心差异2.1 为什么 xml 适合做数据交换却不适合直接喂给 YOLOPASCAL VOC 的 xml 格式本质上是一份“人类可读”的标注清单。它的结构是树形的根节点是 annotation下面挂着 folder、filename、source、size、object 等子节点。每个 object 节点里又包含 name类别名、pose、truncated、difficult 以及一个 bndbox 边界框bndbox 里是 xmin、ymin、xmax、ymax 四个整数坐标。这种格式的设计初衷是给人看的方便你用 LabelImg 打开检查标注质量也方便不同论文之间交换数据。它的问题也很明显坐标是绝对像素值分辨率一旦改变所有标注全部失效而且 xml 文件本身有大量重复的元信息比如 folder、source对训练来说全是冗余。YOLO 的 txt 格式则完全是另一套思路。每个 txt 文件对应一张图片文件名和图片名保持一致每行代表一个目标格式是类别id x_center y_center width height。注意这四个值全部是相对于图片宽度和高度的归一化浮点数取值在 0 到 1 之间。这种设计的直接好处是模型在训练时不需要关心输入图片的原始分辨率不管你是 1920×1080 还是 416×416归一化坐标都能直接使用。2.2 一份数据同时给 xml 和 txt常见的两种目录组织方式拿到这份数据集时你首先要看的是目录结构。常见的做法有两种。第一种是标准 VOC 布局dataset/ ├── annotations/ # 存放所有 xml 文件 ├── images/ # 存放所有 jpg/png 图片 └── labels/ # 存放由 xml 转换出来的 txt 文件第二种是带划分的布局适合直接训练dataset/ ├── train/ │ ├── images/ │ └── labels/ ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/如果你拿到的是第一种布局那需要自己划分 train/val如果是第二种直接就能开始训练。我倾向于在拿到数据后先写一行命令看下文件数量# 统计 xml、图片、txt 三类文件的数量确认数据是否完整 find . -name *.xml | wc -l find . -name *.jpg -o -name *.png | wc -l find . -name *.txt | wc -l如果图片数明显多于 xml 数说明部分图片没有标注训练时这些图片会被 YOLO 自动跳过如果 txt 数少于 xml 数说明转换过程有遗漏。这个检查只要 10 秒但能帮你省掉后面排查“为什么训练 loss 异常”的大量时间。2.3 类别 ID 的映射关系这是最容易出错的隐性坑不管是 xml 还是 txt最终训练时模型只认类别 ID不认类别名。xml 里写的是 “traffic light”“speed limit 50” 之类的字符串YOLO 训练需要的是一个 classes.txt 文件里面按行列出所有类别名行号就是类别 ID从 0 开始。这份数据集的 txt 文件里第一列数字是 0、1、2 还是 5取决于作者在转换时怎么排列类别名。你拿到数据后的第一件事应该是打开一个 txt 文件看一眼类别 id 的分布再打开对应的 xml 确认类别名把映射关系搞清楚。我见过有人直接把自己数据集的 classes.txt 换成数据集自带的结果类别名和 id 对不上模型训出来 loss 很低但检测结果全是乱的。这里给出一个快速检查类别映射的脚本import os from collections import Counter # 扫描 labels 目录下所有 txt统计每个类别 id 的出现次数 label_dir labels counter Counter() for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname), r) as f: for line in f: cls_id line.split()[0] counter[cls_id] 1 print(类别 ID 分布:, dict(sorted(counter.items())))这段代码会输出类似{0: 2341, 1: 1890, 2: 980}的结果。拿到分布后你需要去 xml 文件里确认 id 0 对应的是 “traffic light” 还是 “speed limit”这一步直接决定你后续 classes.txt 怎么写。3. 把 xml 转成 YOLO txt转换脚本与四个必须处理的边界坑3.1 基础转换脚本坐标归一化与文件一一对应如果你拿到的数据只有 xml 没有现成的 txt那么第一件事就是写转换脚本。YOLO 需要的归一化坐标计算公式如下x_center (xmin xmax) / 2 / widthy_center (ymin ymax) / 2 / heightbox_width (xmax - xmin) / widthbox_height (ymax - ymin) / height这里 width 和 height 取自 xml 里的 size 节点不是图片解码后重新算的。直接用 xml 里的值可以避免解码大图带来的额外 IO 开销。下面是一个我常用的转换脚本import os import xml.etree.ElementTree as ET def convert_xml_to_yolo(xml_path, label_dir, class_names): 将单个 VOC xml 文件转换为 YOLO txt 标注 xml_path: xml 文件路径 label_dir: txt 输出目录 class_names: 类别名列表索引即类别 ID tree ET.parse(xml_path) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) txt_name os.path.splitext(os.path.basename(xml_path))[0] .txt out_path os.path.join(label_dir, txt_name) with open(out_path, w) as f: for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in class_names: continue # 跳过未在类别列表中的目标 cls_id class_names.index(cls_name) 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) x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) # 使用示例 class_names [traffic light, speed limit, stop sign] # 需根据数据集实际情况调整 xml_dir annotations label_dir labels os.makedirs(label_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): convert_xml_to_yolo(os.path.join(xml_dir, xml_file), label_dir, class_names)脚本逻辑很直接解析每个 object 节点提取类别名和 bndbox 坐标按公式归一化后写入 txt。需要注意class_names.index(cls_name)这行如果 xml 里有类别名不在你的列表里程序会直接报错所以前面加了一个if cls_name not in class_names的过滤。关于边界框坐标xml 里存的不一定是整数。有些标注工具会输出浮点数比如被标注物体边缘正好落在像素之间的情况。脚本里统一用 float() 解析这样不管原始数据是整数还是浮点都能处理。还有一个容易被忽略的点有些 xml 里的 size 节点缺失或者 width/height 字段为 0。这在从网上爬图自动标注的数据里很常见。遇到这种情况脚本会因为除零错误直接中断。我一般会在读取 size 后加一个保护判断如果 width 或 height 为 0就用 PIL 打开图片获取真实尺寸from PIL import Image if width 0 or height 0: img_path os.path.join(image_dir, os.path.splitext(os.path.basename(xml_path))[0] .jpg) img Image.open(img_path) width, height img.size3.2 边界坑一difficult 和 truncated 目标是否保留VOC 标注体系中object 节点下可能有truncated1/truncated、difficult1/difficult这样的标记。前者表示目标超出图片边界被截断后者表示目标难以辨认。在 YOLO 训练中这两类目标默认是应当保留还是剔除业界没有统一答案取决于你的场景。对于红绿灯检测我认为 truncated 目标应当保留。原因是真实道路场景中红灯经常处于画面边缘——比如右转车道上的信号灯只拍到了半边。如果你把这些截断样本都删了模型对边缘目标的召回率会明显偏低实际测试时会出现“该刹车的红灯没检测到”这种致命漏检。而 difficult 目标通常是因为遮挡、模糊、远距离导致标注质量存疑这些样本保留在训练集里会让 loss 在训练初期剧烈震荡。我处理这两类目标的默认策略是truncated 保留difficult 在转换时写入一个单独的 ignored 目录不参与训练。如果你想要更精细的控制可以在转换时给 YOLO 增加一个类别把 difficult 目标统一标成这个“ignore”类推理时过滤掉即可。3.3 边界坑二类别不平衡红绿灯数量碾压交通标志交通标志和红绿灯的数据天然存在数量级差异。一个十字路口可能只有 4 组红绿灯但周围有几十个标志牌或者反过来一条高速公路上一路都是限速牌信号灯只在匝道口出现一次。如果直接拿全部数据训练模型会严重偏向样本量大的类别。处理方式不是简单地删样本而是先用上面的 Counter 脚本把类别分布拉出来再用以下策略之一做平衡上限采样对样本量最多的类别随机抽样限制其不超过某个阈值比如所有类别中位数的 1.5 倍复制增强对样本量最小的类别通过轻微的水平翻转、亮度调整复制增强到接近中位数类别权重在 YOLO 的 loss 计算中按类别样本量倒数设置权重让模型更关注小样本类别。实际操作中第一种策略最简单、效果也最可控。直接改数据分布不需要动训练代码。3.4 边界坑三xml 文件名和图片文件名不一致导致训练时图像与标签配对失败有些数据集在整理过程中xml 和图片的名字并不完全一致。常见的情况包括图片是frame_0001.jpgxml 是frame_0001.xml这种没问题但也可能图片叫img001.pngxml 却叫img001_1.xml或者图片文件名带后缀而 xml 不带。一旦不一致YOLO 训练时找不到对应标签会直接跳过该图片。面对这种情况最稳妥的做法是写一个校验脚本import os image_dir images label_dir labels img_names {os.path.splitext(f)[0] for f in os.listdir(image_dir) if f.endswith((.jpg, .png))} label_names {os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith(.txt)} # 找出有图片但没有对应标签、有标签但没有对应图片的样本 no_label img_names - label_names no_image label_names - img_names print(f有图片无标签: {len(no_label)} 个) print(f有标签无图片: {len(no_image)} 个) if no_label: print(示例:, list(no_label)[:5])如果发现 no_label 数量较多优先检查是不是图片格式不统一——例如同批次数据里部分图片是 jpg部分是 png而 xml 里记录的 filename 写死了扩展名。在转换脚本里把 “filename 字段与磁盘实际文件做匹配” 这一步加上能解决大部分问题。3.5 边界坑四归一化坐标越界YOLO 的坐标归一化之后值域理论上应该在 0 到 1 之间。但由于标注时人工框选往往比实际物体略大一圈或者物体本身就贴边转换后可能出现 x_max / width 1 或 x_min / width 0 的情况。这类坐标越界会导致训练 loss 出现 NaN 或者模型收敛不稳定。我处理越界的策略是轻度越界超出部分小于边界框宽度的 5%直接 clamp 到 0 和 1重度越界则跳过这个目标。实现上就是加一个裁剪逻辑x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0)需要在裁剪前判断原始目标面积是否合理。比如一个目标本来宽 0.2因为越界被裁到 0.01那这个样本基本是废的留着反而污染训练数据。简单退出逻辑是裁剪后如果 w 或 h 小于原始值的 50%就放弃该目标。4. 用这份数据训练 YOLO目录划分、配置文件与训练命令实战4.1 按 8:2 划分 train/val保证同帧数据不泄漏是关键训练前第一步是划分数据集。对于视频抽帧得到的交通场景数据有一个特别容易犯的错同一个视频连续帧被分到了 train 和 val 中。这样 val 里出现的画面和 train 高度相似验证指标会虚高到你不敢相信但部署到真实场景就原形毕露。划分数据时最好按视频片段编号来划分而不是逐文件随机划分。比如原始文件名里带有 frame_0001、frame_0002 这样连续编号的先按前缀分组再在组级别做随机划分import os import random from collections import defaultdict image_dir images video_groups defaultdict(list) for fname in os.listdir(image_dir): if fname.endswith((.jpg, .png)): # 假设文件名格式为 video1_frame_0001.jpg取出视频编号部分 video_id fname.split(_frame_)[0] video_groups[video_id].append(fname) all_videos list(video_groups.keys()) random.seed(42) random.shuffle(all_videos) split_idx int(len(all_videos) * 0.8) train_videos all_videos[:split_idx] val_videos all_videos[split_idx:] # 根据视频分组生成 train/val 文件清单 train_images [f for v in train_videos for f in video_groups[v]] val_images [f for v in val_videos for f in video_groups[v]] with open(train.txt, w) as f: for img in train_images: f.write(os.path.join(image_dir, img) \n) with open(val.txt, w) as f: for img in val_images: f.write(os.path.join(image_dir, img) \n)这段脚本的核心在于video_id fname.split(_frame_)[0]这行需要根据你数据集实际的文件命名规则调整分隔符。如果文件名没有明显的视频编号可以检查 EXIF 信息或者拍摄时间戳来分组。4.2 YOLOv8/v11 的 data.yaml如何正确指向 txt 标签目录无论你用 Ultralytics 的 YOLOv8 还是新出的 YOLOv11都需要一个 data.yaml 文件来告诉模型类别有哪些、训练数据在哪里、验证数据在哪里、标签用什么格式。这个文件是最容易配置错的地方。常见的错误包括path 路径用了相对路径导致找不到数据、train/val 指向的是图片目录而不是文件清单、nc 数量与类别数不一致。# data.yaml path: /your/absolute/path/to/dataset train: train.txt # 指向图片路径清单 val: val.txt # 指向图片路径清单 nc: 3 names: [traffic light, speed limit, stop sign]一个关键细节是 train/val 字段指向的文件格式。Ultralytics 支持两种写法一种是直接写图片目录另一种是写包含图片路径列表的 txt 文件。上面的例子用的是 txt 文件清单方式这时 data.yaml 里的路径要写实际的文件路径而不是目录。还有一种常见情况是标签和图片在同一个目录但文件名不一致。Ultralytics 在训练时会用图片名去标签目录里找同名 txt如果找不到就认为该图片没有目标直接跳过。检查是否正常匹配最简单的办法是跑一遍验证脚本看看训练日志中显示的图片数量和实际数量是否一致。4.3 用 YOLOv8/v11 开始训练选对模型尺寸和超参数YOLO 系列模型从 n、s、m、l、x 五个尺寸参数量和推理速度依次递增。对于交通标志和红绿灯这种小目标密集的场景我不建议一上来就用最大的 x 模型。这类任务的特点是目标小但纹理简单标志牌上的文字才是区分不同类别的主要特征。# 使用 YOLOv8s 训练640 输入尺寸50 个 epoch yolo detect train \ modelyolov8s.pt \ datadata.yaml \ imgsz640 \ epochs50 \ batch16 \ device0参数选择逻辑如下imgsz 用 640 是默认值但交通标志这种小目标可以尝试 960 或 1280代价是显存占用翻倍。如果显卡只有 8GB建议先用 640 跑通再用 960 对比效果。batch 大小取决于显存。12GB 显存跑 yolov8s 用 batch 16 没问题6GB 显存建议降到 8否则会 OOM。epochs 根据数据集规模定。几千张图片 50 个 epoch 基本够用如果 loss 还在明显下降就继续训如果 val loss 已经开始回升那就是过拟合了早期停止是正确选择。另外建议打开save_period参数每个 epoch 都存一份权重方便回溯。YOLO 训练中断后可以用相同命令 resume它会自动读取最新的last.pt。4.4 训练失败的排查先看 loss 再看 mAP最后才怀疑代码训练过程中如果发现异常不要急着重装环境或换模型按顺序排查第一步看 loss。如果 loss 从一开始就是 NaN先检查 labels 里有没有空文件、有没有坐标异常值。用这个命令快速检查# 找出所有内容为空的 txt 文件 find labels -name *.txt -size 0 -print第二步看 loss 是否震荡不降。这通常是类别不平衡或标签噪声导致的回看第 3 章的类别分布统计检查是不是某个类别样本太少。第三步看 mAP。训练完成后 mAP 很低但 loss 也低这大概率是标签和类别映射错位。重新检查 classes.txt 和你写进 data.yaml 的 names 是否一一对应。5. 数据使用避坑红绿灯与交通标志训练里最容易翻车的五件事5.1 类别名大小写和空格不一致导致同一个目标被拆成两类xml 标注由不同人员协作完成时很容易出现 “Traffic Light”“traffic light”“traffic_light” 三种写法。转换脚本中如果直接用类别字符串做映射这三种会被认定为三个不同的类最终导致模型无法区分同一个目标。解决方式是在转换前统一做规范化全部转小写、空格转下划线、去除字符串首尾空白。做一次类别名清洗能避免后续所有与类别映射相关的连锁问题。5.2 夜间红灯与白天红灯的视觉差异会让模型只学会一种光照条件红绿灯检测在白天和夜晚的表现差异是所有从业者都会遇到的难题。夜晚的红灯在画面中呈现为明亮的光晕而白天的红灯饱和度较低、轮廓清晰。如果你的数据集中夜间图片占比过低模型会在夜间场景下频繁漏检或误检。解决方案是在训练前检查数据集中白天/夜晚图片的比例。如果夜间数据不足用图像增强来凑但要注意普通的 HSV 增强提高亮度很容易把画面变得不自然。更好的做法是用 MixUp 或 Mosaic 增强让模型同时看到不同光照下的目标上下文。如果你用的是 YOLOv8训练时增大 hsv_v饱和度和 hsv_h色相的增强幅度能模拟一部分光照变化。5.3 远距离小目标丢失目标在 640 分辨率下只有 8×8 像素交通标志检测中最典型的失败案例实际场景中标志距离车 50 米时在 640×480 的画面里可能只占 15×15 像素。YOLO 的 head 在 8 倍下采样后的特征图上这个目标只对应到约 2×2 的网格单元信息量严重不足。针对这个问题除了提高输入分辨率更重要的是用好 YOLO 的多尺度训练。在训练参数中增加scale0.5和scale1.5让模型在训练时看到不同尺度的目标。另外可以用--augment开启额外的随机透视变换让小目标在特征图中更难被忽略。5.4 验证集里出现同一地点的不同帧导致 mAP 虚高我在第 4 章提到过按视频分组划分数据这里再强调一遍。很多红绿灯数据集是从路口监控视频抽帧得来的前 100 帧是 4 号路口后 100 帧是 7 号路口。如果随机打乱后划分 train/val极有可能出现同一个路口的画面同时出现在训练集和验证集的情况。这种情况下的 mAP 没有任何参考意义。判断依据是如果训练集 mAP 有 0.85但你在真实场景测试时只有 0.5 左右先回去检查数据集划分而不是怀疑模型结构。一个更严格的划分方式是按地点划分——确保任何一张验证集图片的地点都不出现在训练集中。这对交通场景数据尤其重要因为背景信息路面、建筑、树冠对检测结果的影响比想象中大得多。5.5 标签文件与图片不在同一目录下导致 YOLO 训练时批量跳过Ultralytics 的设计逻辑是图片在images/train/标签必须在images/train/的同级目录或由label_dir参数指定的位置。默认情况下它在图片同目录寻找同名 txt。如果你的数据是图片和标签分开存放的比如图片在dataset/images/标签在dataset/labels/直接训练会导致大量图片被跳过。解决办法是在 data.yaml 中补上标签路径path: /your/absolute/path/to/dataset train: train.txt val: val.txt label_dir: labels或者干脆把标签软链接到图片目录下# 在图片目录下创建指向 labels 目录的软链接 ln -s /path/to/labels /path/to/images/labels检查标签是否正常匹配最直观的方式是把训练日志里 “labels” 那一行输出的数字与你的标注总数比较。这个数字通常在每条 epoch 日志的头部如果明显偏小说明有标签没被加载。6. 红绿灯检测的进阶验证按颜色时序做逻辑校验比单纯看 mAP 更有用在模型训练完、 mAP 达标之后还有一个我常用的验证步骤利用红绿灯本身的时序逻辑来做逻辑校验。红绿灯和普通物体检测最大的区别在于它带有状态属性——一个路口同一组的红灯和绿灯状态在大多数时间段是不可同时为真的。基于这个事实你可以写一个简单的时序校验脚本作用是用代码检查模型输出的状态是否违反基本的交通逻辑。从视频序列取连续 50 帧的推理结果逐帧记录红灯和绿灯的置信度。一个健康的模型输出应该呈现交替状态而非频繁闪烁。如果出现连续帧中红灯绿灯同时输出高置信度比如在一个红绿灯检测框内同时得到 red0.8 和 green0.7说明模型对颜色状态的判别不够稳定。这时不急着调模型先检查数据集的标注中对颜色状态的划分方式。如果数据集把红、黄、绿分别作为独立类别那类别数通常会是“trafficLight_red”、“trafficLight_green”、“trafficLight_yellow”这样的形式。模型输出的红绿类别天然互斥逻辑校验一般没有问题。但如果数据集中将“红绿灯”作为一个整体类别没有颜色子类那模型实际上是用一个类别框同时覆盖了红、黄、绿三种状态这时时序校验就变得极其重要——你需要用额外的分类器或者像素统计来区分颜色。我在做这类验证时的一般做法是在推理代码里加入一个简单的后处理逻辑import cv2 import numpy as np def validate_light_color(frame, bbox, crop_size32): 用 HSV 色彩空间统计红绿灯框内主色调 返回 red / green / yellow / unknown x1, y1, x2, y2 bbox crop frame[y1:y2, x1:x2] if crop.size 0: return unknown resized cv2.resize(crop, (crop_size, crop_size)) hsv cv2.cvtColor(resized, cv2.COLOR_BGR2HSV) red_mask cv2.inRange(hsv, (0, 100, 100), (10, 255, 255)) \ cv2.inRange(hsv, (160, 100, 100), (180, 255, 255)) green_mask cv2.inRange(hsv, (35, 100, 100), (85, 255, 255)) yellow_mask cv2.inRange(hsv, (20, 100, 100), (35, 255, 255)) red_pixels np.sum(red_mask 0) green_pixels np.sum(green_mask 0) yellow_pixels np.sum(yellow_mask 0) color_counts {red: red_pixels, green: green_pixels, yellow: yellow_pixels} dominant max(color_counts, keycolor_counts.get) return dominant if color_counts[dominant] 30 else unknown之所以用 HSV 而不是 RGB 来判断颜色是因为红绿灯在夜晚会出现红色光晕RGB 空间内红的饱和度信息和亮度信息混在一起HSV 的色相通道能更稳定地区分红与黄。上面这个脚本用了两段红色掩码是因为 HSV 中红色的色相范围在 0 附近不连续需要拼接两个区间。这类后处理逻辑做完之后你可以从 val 集中抽出包含红绿灯的图片用模型跑一次检测再用上面的脚本对每个检测框做颜色二次确认。如果二次确认的结果和模型直接输出的类别不一致说明模型对颜色的判别更多依赖上下文而不是灯本身的颜色特征需要回看训练数据中是否颜色多样性不足。还有一个属于习惯性的收尾动作每次训练完除了保存权重和 data.yaml我会把验证集预测结果用 YOLO 自带的val结果的混淆矩阵存下来。这个混淆矩阵能清晰地展示模型把红灯看成绿灯、把限速 30 看成限速 50 这类具体错误比盯着 mAP 数字更能指导下一步是补数据还是改配置。红绿灯检测最终检验模型是否可靠的是它在连续帧里的行为是否符合真实交通逻辑图形指标本身只是中间过程。这套用逻辑时序反推模型行为的验证思路同样适用于车流检测、行人过街等带明显状态变化的任务。希望帮到你。本文还有配套的精品资源点击获取
返回列表