ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于YOLO的红绿灯识别实战:从数据清洗到训练调参的完整指南

基于YOLO的红绿灯识别实战:从数据清洗到训练调参的完整指南 简介面向计算机视觉与自动驾驶场景这份红绿灯识别数据集专为YOLO系列目标检测模型训练而整理覆盖红灯、绿灯、黄灯等常见信号灯状态能直接作为模型训练与验证的标准输入。压缩包共含2000个文件其中1998个XML标签文件采用VOC格式标注信号灯位置与类别另附一个TXT标注文件和一个Python脚本用于快速统计标签类别及数量整体大小644.58MB。在YOLO训练过程中XML/TXT标注可直接用于监督学习配合脚本能检查样本均衡性、避免类别不平衡帮助模型稳定识别不同灯色。目前已有298人学习下载适合目标检测入门者、算法工程师及智能交通项目开发者省去从零采集与标注大量图像的繁琐流程直接聚焦于模型调试、精度优化与部署验证。 很多搞视觉检测的朋友一开始都会选红绿灯识别练手原因很简单场景常见、类别固定、需求明确。但真上手了才发现这个看似“入门级”的任务有不少隐藏的坑。市面上公开的红绿灯数据集大多带着国外路况的印记信号灯样式、安装位置、配时逻辑和国内差异很大直接拿过来训迁移到自家场景时精度掉得厉害。我最近基于YOLO框架跑了一轮红绿灯识别从数据准备、清洗、格式转换到训练调参完整走了一遍这篇就把其中的关键环节和踩过的坑梳理一下给正在做类似项目的朋友一些参考。这个项目核心就三件事构建一个可靠的红绿灯数据集、把数据处理成YOLO可用的格式、训练一个能落地使用的检测模型。适合正在入门目标检测的学生、需要做交通场景识别的工程师以及想了解工业级数据怎么“清洗”的人阅读。下面直接讲实操。1. 数据层面红绿灯数据集的选型与自建1.1 公开数据集盘点哪些能用哪些只能参考先聊聊公开数据集。目前圈子里常用的有这么几个Bosch Small Traffic Lights Dataset德国路况标注的是小尺寸信号灯分辨率低噪声大适合测模型在小目标上的表现。这套数据偏“硬核”训练时loss经常被小目标带偏。LISA Traffic Light Dataset美国加州路况包含不同天气和光照类别有go、warning、stop等标注相对规整适合做预训练或在复杂光照下做微调。DriveU Traffic Light Dataset德国多路口有视频序列时间连续性好适合做时序相关的优化但单帧多样性一般样本之间有很强的相似性。TT100K国内腾讯实验室出的交通标志数据集虽然主打标志识别但也有部分信号灯标注可以作为补充数据源尤其是国内路口的样式可以参考。我自己的体验是公开数据集可以拿来做预训练或可行性验证但要落地到具体项目比如某城市的特定路口最好按自己的场景重新标注一批数据。原因很简单——各国各地区的红绿灯在排列方式、灯组形状、倒计时显示乃至安装高度上都有差异模型必须见过“你家路口的样子”才能在你的场景里表现稳定。1.2 自建数据集的采集与标注细节自建数据最稳妥的方式是从行车记录仪视频中抽帧。这里有个建议不要按时间均匀抽帧而是按场景变化抽帧。你可以在视频上每隔10到20帧做一次画面相似度比对画面变化超过阈值才保留。否则连续几十帧几乎一模一样的画面会让训练集里充满冗余样本白白浪费显存和训练时间。标注工具我用的X-AnyLabeling它是半自动标注工具可用已有的YOLO模型先标一遍人工只做修正。这能让标注速度提升三到五倍。不过要留意红绿灯本身就是小目标自动标注很容易把远处的红灯漏掉或把刹车灯误标成红灯所以人工复核这一步省不了。标注规范建议按这个来类别名用单数英文red、green、yellow不要用red_light这类带下划线的名字YOLO读取时虽然不报错但在一些转格式脚本里容易出问题。只标“亮着的灯”不标灯壳。红绿灯识别本质上是在检测“当前信号状态”灯壳不提供状态信息标进去反而增加干扰。对于箭头灯建议拆成red_left、green_left这类类别或者统一归入red、green但通过角度判断方向。前者训练更直接后者需要后处理。如果你的场景有明确左右转需求建议前者。标注完一定要做画框检查——把标注结果画回图片上一张张快速翻看重点看三类错误框位偏移、漏标、类别错标。这个步骤虽然原始但比任何自动化检查都有效。1.3 数据清洗与类别平衡的实操心得数据清洗大概率比你想象的花时间。我第一轮清洗大约处理了8000张图最后留下了约6200张剔除了将近四分之一。剔除的主要是这几类严重过曝或欠曝的夜间图、红灯与刹车灯混淆的图、遮挡面积超过50%的图、以及对焦模糊的图。洗完数据还要做类别平衡。红绿灯场景里的类别天然不平衡红灯和黄灯出现频率高绿灯和左转箭头相对少。不平衡会导致模型对高频类过拟合低频类精度掉得厉害。解决方式主要有三种对低频类别做过采样即在每个epoch中让低频类图片多出现几次对低频类别做数据增强比如复制粘贴小目标Copy-Paste把绿色的灯贴到其他无灯场景中注意贴的时候要按透视关系缩放不能直接硬贴用加权损失函数给低频类更高的loss权重。我最后实际采用的是“过采样适度Copy-Paste”的组合没有动损失函数因为YOLOv8的损失函数改动会影响其他参数风险比较大。注意Copy-Paste增强建议只在训练后期开启且粘贴数量要控制。前期模型还没学到基础特征贴太多合成样本容易让模型学到“绿点贴在路边”这种伪特征。2. 从原始标注到YOLO格式格式转换不能用现成脚本一把梭2.1 理解YOLO标注格式的本质YOLO格式看起来很简单每行五个数字依次是类别id、归一化中心点x、归一化中心点y、归一化宽w、归一化高h。但很多人栽在“归一化”这三个字上——这里的归一化不是除以整张图的宽高而是除以当前图片的实际宽高。有个常见错误从LabelMe导出的JSON里坐标可能是多边形顶点也可能基于原图绝对像素标签文件里存的是原始坐标而训练时读入的图片被resize过最后框的位置就全偏了。正确做法是把所有坐标统一到0-1范围后存储代码里尽可能不做resize或者resize完同步更新标注坐标。2.2 转换脚本的数据校验与边界处理我自己写转换脚本时主要做了五件事import os import cv2 import numpy as np import json def convert_labelme_to_yolo(json_path, img_dir, out_dir, class_names): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_path os.path.join(img_dir, data[imagePath]) img cv2.imread(img_path) if img is None: return None h, w img.shape[:2] lines [] for shape in data[shapes]: label shape[label] if label not in class_names: continue class_id class_names.index(label) points shape[points] # 取外接矩形 xs [p[0] for p in points] ys [p[1] for p in points] xmin, xmax min(xs), max(xs) ymin, ymax min(ys), max(ys) # 边界裁剪与过滤框完全超出图片则跳过 if xmax 0 or ymax 0 or xmin w or ymin h: continue xmin max(0, xmin) ymin max(0, ymin) xmax min(w, xmax) ymax min(h, ymax) # 过滤过小目标小于5x5像素直接丢弃 if (xmax - xmin) 5 or (ymax - ymin) 5: continue # YOLO格式归一化 x_center (xmin xmax) / 2 / w y_center (ymin ymax) / 2 / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h # 过滤越界框中心点必须在图内 if not (0 x_center 1 and 0 y_center 1): continue lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) if not lines: return None img_name os.path.splitext(os.path.basename(img_path))[0] out_txt os.path.join(out_dir, img_name .txt) with open(out_txt, w) as f: f.write(\n.join(lines)) return len(lines)这段代码里几个关键点说一下边界裁剪标注时框稍微出了画面边缘是常有的事直接把超出部分裁掉让中心点和宽高仍落在图内。不裁剪的话训练时会警告还会影响损失计算。过小目标过滤红绿灯在远距离时可能只有3-5个像素。保留它们会让模型学习困难因为下采样之后那几个像素可能就消失了完全丢弃又会让数据集失去“远距离检测”场景。我测试下来的折中是小于5像素的丢弃5到15像素的保留并靠增强弥补。中心点越界过滤YOLO的坐标逻辑要求中心点必须在图内。如果外接框被裁得只剩一个小角中心点会落在图外这种样本需要剔除。2.3 数据集的目录组织与训练集/验证集划分目录结构建议严格按照YOLO的标准来datasets/ ├── traffic_light/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ ├── labels/ │ │ ├── train/ │ │ └── val/ │ ├── data.yaml有个很多人忽略的点划分训练集和验证集时尽量以“路口”或“视频片段”为单位而不是以单张图像为单位。同一路口的连续帧画面高度相似如果这些相似帧被同时分进训练集和验证集验证指标的参考价值会大打折扣。简单说就是“验证集泄题”了。具体实现上先看每张图属于哪个视频序列按视频序列为单位做随机划分保证同一个视频的帧不会跨训练/验证边界。这个逻辑用代码实现不复杂但效果差异巨大。2.4 data.yaml 配置文件的几个坑配置文件里最容易出问题的就是路径和类别顺序。path: ../datasets/traffic_light train: images/train val: images/val nc: 3 names: [red, green, yellow]“nc”写错很致命。YOLOv8虽然在训练时会自动根据names长度推断类别数但依赖的是“配置文件里的nc”还是“检测到的names长度”不同小版本之间行为不太一致。建议显式同时写对nc和names且names中的顺序要与标注txt里的类别id一一对应——这是最容易造成“训练时loss正常、推理时乱标”的原因。另外注意data.yaml里的path路径不要写绝对路径。你的数据集如果跟着项目一起迁移绝对路径会让别人的环境直接跑不起来。写相对路径或依赖YOLO命令中的“--data”参数指定迁移性会好很多。3. 训练细节YOLOv8与YOLOv5的选型、超参与环境配置3.1 版本选型选v8还是继续用v5现在做目标检测新项目我一般直接推荐YOLOv8原因有三内置的C2f结构和Anchor-Free机制对小目标的检测能力比v5的Anchor-Based强一截数据增强策略更丰富Mosaic、MixUp等是内置的不需要额外开发Ultralytics的API设计很清晰训练、验证、导出一站式流程尤其适合快速迭代。当然如果你的部署环境是老旧设备或需要TensorRT深度优化的模型v5的生态更成熟算子支持也更好。但单纯从红绿灯检测这个任务来说v8的收益更明显。3.2 模型尺寸与硬件的匹配说到硬件很多人跑来问“AMD 580显卡能跑YOLO吗需要装CUDA吗”这类问题。这里把话说透AMD显卡不能直接用CUDA但YOLO可以靠CPU跑只是速度会慢不少。如果你是非NVIDIA显卡编译时用CPU版本PyTorch然后纯CPU训练对于resize到640×640的红绿灯小数据集v8nnano版也不是不能跑但一个epoch可能要几分钟。如果你手头是NVIDIA显卡比如3060或以上那就安装对应版本的CUDA和cuDNN训练速度会有质的飞跃。选模型大小时有个判断标准先看你的目标和算力再看精度需求。红绿灯检测属于小目标任务建议直接用v8m或v8l除非确实显存不够再退到v8s。我用6GB显存的卡跑v8sbatch size可以从默认值降到8图片resize到640能稳定训练完。如果换了v8l就得用梯度累积模拟大batch否则Out of Memory会频繁出现。3.3 训练常见参数的设置与调节逻辑下面是我在红绿灯数据上实际用过的参数配置你可以作为参考基线yolo detect train \ --model yolov8s.pt \ --data data.yaml \ --epochs 300 \ --batch 16 \ --imgsz 640 \ --optimizer AdamW \ --lr0 0.001 \ --lrf 0.01 \ --patience 30 \ --mosaic 1.0 \ --mixup 0.2简单解释一下epochs设为300红绿灯数据集一般来说几千张图300轮足够充分收敛配合早停机制patience30防止过拟合。batch设为16这是个相对保守的值。batch size增大通常能提升稳定性但显存不够时用梯度累积accumulate参数比强上大batch更稳妥。optimizer用AdamWYOLOv8默认是SGD但在中小规模数据集上AdamW的收敛速度明显更好最终的mAP也略高一点。这个差异在热力图上看最直观。mosaic与mixup拉满小目标检测需要依赖Mosaic增强让模型在不同尺度上见过目标mixup则有助于提升对不同背景的泛化能力。重要提示开启Mosaic时如果数据集里某张图很小比如低于300×300Mosaic增强会将多张图拼在一起再做缩放小图被缩得更小时标注框会失真出现“标注框飘移”。建议在数据预处理阶段把小图统一升级到640×640再入库或者训练时把mosaic概率降低。3.4 损失函数与训练日志的解读YOLOv8的损失由三部分组成分类损失BCE、框回归损失CIoU或DFL和置信度损失。训练时终端输出的loss是三者加权后的平均。多数人只看这个总loss但排查问题时一定要分开看yolo detect train ... --verbose开启verbose之后可以看到每个子loss的走向。如果分类损失降但回归损失不降问题多半在标注框质量上——可能框贴得太紧或边界裁剪造成了误差。如果回归损失降但分类损失不降就要检查类别平衡以及是否存在相似类比如红色刹车灯被标成了红灯。另外训练日志里那个box_loss如果出现剧烈震荡通常和batch size太小或学习率太高有关。可以先降学习率到原来的十分之一试一试不要一上来就换模型结构。4. 常见问题与排查技巧实录4.1 训练时Loss不下降测试过的排查路径我在这个项目里遇到过loss徘徊在1.2左右不动的现象。排查顺序如下先确认标注txt里的归一化坐标没有异常值——打印几个样本出来看比如class 0 0.52 0.33 0.03 0.08数值必须都在0-1之间。再确认数据加载没有报warning——YOLO在过滤掉异常标注框时会打印skipping信息如果大量跳过说明标注文件有问题。然后把学习率从0.001降到0.0001试试——如果是学习率太大导致的震荡这一步通常能立竿见影。最后把数据增强全部关掉跑20个epoch做对照实验——如果关闭增强后loss能降说明增强强度太大或者原始标注有噪声增强把噪声放大了。4.2 小目标漏检率高从增强、尺度、锚框三个角度解决红绿灯识别最典型的问题就是远距离小目标漏检。图像里远处信号灯只有十几个像素模型很难分辨。除了前面提过的保留小目标标注之外还有几个手段比较有效提高输入分辨率从640提高到960甚至1280小目标的像素数变多特征提取更有依据。代价是训练时间增加约两倍显存占用也明显变大。开启多尺度训练YOLOv8默认会在一定范围内随机缩放输入尺寸这本身对小目标有帮助。如果你关闭了这个选项建议打开。调整anchor仅限v5v8不需要YOLOv8是anchor-free不用手工调anchor。如果是v5可以先用遗传算法在自有数据集上重新聚类anchor比默认的coco anchor更适合红绿灯这种“又小又长”的目标形态。4.3 模型在夜间表现差给训练集“补课”而不是盲目调模型夜间是红绿灯识别最容易翻车的场景。灯本身亮着但光晕严重、周围环境暗、过曝的问题全挤在一起。单纯调模型结构解决不了本质问题核心还是数据把夜间图像用图像增强工具提亮一档但注意不要硬拉——拉太亮会把红灯的红色信息破坏掉。适当加入一些“光晕边界”模糊的样本让模型学习“即使灯轮廓不清晰颜色特征仍然是核心依据”。如果检测画面里经常出现LED屏幕或车尾灯可以在后处理时加上位置先验——信号灯一般位于车道上方或路侧高处如果检测框落在画面底部直接按低置信度过滤掉。我自己在夜间验证集上的mAP从0.62提升到0.81主要靠的就是扩充夜间样本和负面样本而不是继续加大模型规模。数据对了模型才有得学。4.4 相邻目标粘连调整NMS参数的小技巧红绿灯有时近距离的多个灯组会同时亮起比如红灯和黄灯同时存在转换期这时检测框容易粘在一起被NMS合并成一个框。遇到这种问题可以在后处理推理时调低NMS的IoU阈值从默认的0.45调到0.3能减少粘连框被误合并的情况。from ultralytics import YOLO model YOLO(best.pt) results model.predict(sourcetest.jpg, conf0.25, iou0.3, imgsz640)注意这一步可能在输出结果里出现更多的重复框需要在业务后处理中再做一层基于类别的逻辑判断比如“同一位置不可能同时出现两个red灯”通过这个约束做进一步去重。5. 推理部署与落地把模型从笔记本搬到实际场景模型训练完只是第一步。真正落地时要考虑推理速度和输入源的多样性。5.1 模型导出与加速YOLOv8自带导出功能可以直接导出ONNX或TensorRT引擎。在NVIDIA显卡上TensorRT优化后的推理速度能比PyTorch原始模型快2到4倍对小目标检测的精度影响基本可忽略。yolo export modelbest.pt formatonnx opset12 simplifyTrue导完后用ONNXRuntime测试一下输出确认推理结果和PyTorch下一致再考虑做INT8量化但量化时务必在红绿灯数据上重新校准否则精度掉得很厉害。5.2 对视频流的处理建议实际业务中除了单张图片大量场景是视频流。这里容易踩的坑是视频连续帧检测结果抖动剧烈。比如上一帧红灯下一帧突然变绿灯再下一帧又红灯这种抖动在业务上完全不可接受。推荐做法是加上时间平滑class TrafficLightStateFilter: def __init__(self, history_len5): self.history [] self.history_len history_len def update(self, state): self.history.append(state) if len(self.history) self.history_len: self.history.pop(0) # 取众数保证输出稳定 return max(set(self.history), keyself.history.count)核心思路是取最近几帧的检测结果众数作为当前帧的输出能滤掉偶发跳变。更精细的做法是用卡尔曼滤波对检测框位置做跟踪预测不过对大多数场景众数平滑已经够用了。5.3 真实场景中的数据回流模型上线之后不要以为就结束了。要把真实场景中出现的漏检和误检案例定期收集回来补充到训练集里做增量训练。这个闭环比任何一次调参带来的收益都大。我大概是每两周做一次增量训练每次新增200到500张新样本效果稳定提升比反复调超参数靠谱得多。写在最后一点真实体会红绿灯识别这个任务外行人看是个“江湖入门题”做过的人才知道其中遍布细节。数据质量、标注一致性和格式正确性这三件事决定了模型性能的天花板模型结构和训练技巧只是在逼近这个天花板。我个人的经验是与其花大量时间在模型结构上做各种改进不如多花时间在数据层面把问题挖干净。数据对了模型自然就能学好。如果你也在折腾类似的数据集和训练流程希望这篇能帮你少走几步弯路有具体问题也欢迎在留言区交流。本文还有配套的精品资源点击获取
返回列表