首页 / 资讯中心 / 文章详情

交通标志识别项目实战:YOLO多版本适配与数据转换全流程

交通标志识别项目实战:YOLO多版本适配与数据转换全流程 ★ FEATURED ARTICLE
做目标检测的朋友十有八九都拿交通标志数据练过手。原因很现实类别边界清晰、场景相对固定、背景干扰比通用检测好控制跑出来的指标也容易好看。但把模型架子搭起来之后才会发现真正折腾人的不是网络结构而是数据本身以及从数据集到不同版本框架之间反复横跳的适配过程。最近我正好把一个交通标志识别项目从数据整理到多版本 YOLO 验证完整走了一遍里面有不少文档里不会细讲的东西这篇就顺着整个流程拆开说清楚。1. 项目整体设计交通标志识别到底在做什么交通标志识别本质上是目标检测里的一个垂直场景输入是车载摄像头拍到的道路画面输出是画面里每个交通标志的位置框和类别。类别通常包含禁令标志禁止掉头、禁止鸣笛、警告标志连续转弯、注意行人、指示标志直行、右转、限速标志这几大类具体类别数量取决于选用的数据基准。为什么多数团队都会先拿这个场景来做目标检测三个原因第一标志目标尺寸小但语义非常明确适合检验模型在小目标上的召回能力第二实际路况中的光照变化、遮挡、运动模糊都是天然的数据增强素材不用自己费劲造复杂背景第三自动驾驶和辅助驾驶的落地需求一直在增长这个任务做出来之后可以直接往工程方向延伸。项目启动之前我先把需求拆成了三条后面所有动作都围着这三条走数据层面用公开数据集做基础训练再补充一部分自己采集和合成的样本覆盖夜间、逆光、雨天场景。模型层面采用 YOLO 系列做底座因为它在工业界的部署生态最成熟从训练到端侧推理都有现成链路。验证层面同一个数据集在多个主要版本上分别训练和评估对比精度、速度和显存占用选出一个适合当前硬件条件的方案。这里想多说一句选 YOLO 而不是其他检测框架不只是因为性能更关键的是“生态完整”。从标注格式、数据增强到训练脚本、模型导出整个链条都已经标准化了。你只要把数据集转成 YOLO 格式剩下的事情框架基本都帮你包好了。这对于做垂直场景验证来说非常重要能省掉大量工程性杂活把精力聚焦在数据和业务逻辑本身。1.1 数据集的三个硬指标用数据之前先得能判断一套数据到底能不能用。我自己的经验是抓三个硬指标类别覆盖度、样本数量、场景多样性。类别覆盖度决定了模型能不能应对业务里的所有情况。如果只做“限速牌识别”那数据集里只需要限速类别就够了但要做完整的辅助驾驶提醒就必须覆盖禁令、警告、指示、指路牌至少四类否则模型在真实道路上会出现大量漏检。样本数量不是单纯看总数要看每个类别的分布是否均衡。交通标志数据有个通病就是“直行”“禁止通行”这种常见标志样本特别多而“禁止拖拉机”“注意野生动物”这种冷门类别样本可能只有个位数。不处理类别不平衡训练出来的模型会对高频类别过拟合低频类别几乎不检。场景多样性则是保证泛化能力的前提。同样是“限速40”标志晴天正午、夜间路灯、逆光、树干遮挡这四种情况如果训练数据里只有前两种实测必然翻车。1.2 公开数据集的选型思路交通标志识别领域有几个公开的基准数据集为了方便说明这里用代号 A、B、C 来表示你在实际操作时遇到同类数据集可以直接套用下面这套选型逻辑。数据集样本规模类别数标注形式特点A约2万张静态图43类单框目标类间均衡好训练快适合做基线B约10万张多场景图45类单框部分遮挡标注包含不同国家路况风格难度更高C视频抽帧序列28类连续帧标注包含运动模糊和帧间跳跃目标我用 A 做第一轮快速验证理由是它的类别分布相对均匀每类样本都够训练迭代一轮只需要几十分钟。拿到基线指标后再用 B 和 C 做迁移验证看模型在大规模数据和复杂场景下的表现。这里有个容易踩的坑A 里的图片分辨率普遍偏低如果直接把输入尺寸设成 1280可能反而引起过拟合因为小图放大后丢失了很多细节。第一轮建议把训练尺寸保持在 640后续再加分辨率。2. 数据准备与 YOLO 格式转换YOLO 系列虽然版本迭代了很多次但数据接口相对稳定核心不外乎三样东西图片文件、标签文件、数据集配置文件。标签文件是 txt 文本每行代表一个目标格式是“类别id 中心点x 中心点y 宽度w 高度h”坐标全部归一化到 0~1。公开数据集原始的标注格式通常不是这个常见的有 XML 格式和 JSON 格式。XML 里存的是矩形框左上角和右下角的绝对坐标JSON 则可能用四点多边形来表示一个目标区域。所以拿到数据之后的第一件事不是急着开训练而是先把所有标注统一转成 YOLO 的 txt 格式。2.1 从绝对坐标到归一化坐标转换本身不复杂但有个细节很容易被忽略。XML 里的坐标是像素级绝对坐标而 YOLO 需要的是相对图片宽高的归一化比例。如果你直接把绝对坐标喂给训练脚本模型会完全学不进去表现为 loss 不下降或者一路飘红。正确转换过程分三步。第一步解析 XML取出xmin ymin xmax ymax第二步计算中心点和宽高cx (xmin xmax) / 2cy (ymin ymax) / 2w xmax - xminh ymax - ymin第三步全部除以图片宽度和高度得到归一化值。这里还要注意有些标注框会超出图片边界如果 xmin 是负数或者 xmax 大于宽要么裁掉图片、保留框内有效部分要么直接把该样本丢弃千万不要带着越界框训练。2.2 一个可用的转换脚本下面是我在这个项目里实际用过的转换脚本兼容 XML 格式输入输出 YOLO txt。代码不长但能省掉手工改几百个文件的时间。import os import xml.etree.ElementTree as ET from PIL import Image # 自定义类别映射按自己的数据集实际类别调整 class_map { speed_limit_30: 0, speed_limit_50: 1, stop: 2, no_entry: 3, yield: 4, keep_right: 5, # ... } def convert_xml_to_yolo(xml_path, img_path, output_path): tree ET.parse(xml_path) root tree.getroot() img Image.open(img_path) img_w, img_h img.size lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_map: print(f跳过未映射类别: {cls_name}) continue 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) # 处理越界框 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) if xmax xmin or ymax ymin: continue cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h line f{class_map[cls_name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f} lines.append(line) with open(output_path, w) as f: f.write(\n.join(lines)) f.write(\n)这个脚本里包含了越界框的裁剪处理这一步很重要。实际数据集中总会有那么几张图标注得不太规范不处理的话训练时信息增益反而变负。另一个细节是把输出坐标保留六位小数精度太低会导致小目标框位置偏移好几个像素对交通标志这种小目标来说差异很明显。2.3 数据增强策略YOLO 训练时会默认开启一系列增强策略包括随机翻转、拼接、色域变换。针对交通标志场景我额外做了两件事情。第一是加了马赛克增强它会把四张图拼成一张增加单张图片里的目标数量和背景复杂度。这个增强对小目标提升帮助极大因为拼接后目标尺寸相对变小模型被迫提高对特征细节的敏感度。第二是针对夜间和逆光场景做了亮度与对比度扰动。交通标志本身有高对比度的颜色设计训练时如果不加光照扰动模型很容易把颜色当成第一特征一到阴天或者路灯下就失灵。增强也不是越多越好。随机旋转的角度我限制在正负15度以内超过这个范围标志语义会扭曲比如“禁止驶入”的红圈变成椭圆反而干扰模型。垂直翻转我直接关闭了因为交通标志是固定安装的真实的道路画面上不会出现倒立的标志。3. YOLO 多版本适配验证同一份数据集在不同的 YOLO 版本上表现可能差异很大。这个差异不单是精度高低还包括显存占用、训练速度、推理速度、部署难度。我在这个项目里验证了 v5、v8、v10 三个有代表性的版本分别代表成熟稳定、均衡进阶和最新框架三条路线。3.1 不同版本的核心差异v5 是最成熟的生产版本社区资料最多踩坑成本最低。它的 anchor 机制是基于预设框的训练时需要先分析数据集里的框尺寸分布自动适配 anchor。v8 改成了 anchor-free 结构去掉了预设框这个环节训练配置更简单而且换回了更灵活的 C2f 特征提取模块在同等参数下特征表达能力更强。v10 则继续往前走取消了 NMS 后处理把标签分配策略也重写了推理管线更干净但代价是部分旧版代码不能直接迁移。从适配角度讲最直接的差异体现在数据加载配置和超参数上。v5 需要指定anchor相关的配置而 v8 和 v10 不需要v5 的cache参数默认不开而 v8 默认开v10 的batch设置单位含义和 v8 也不完全一致。如果不看版本文档直接复用命令很容易出现报错或者训练效果奇差的情况。3.2 数据配置文件写法三个版本虽然内部结构不同但数据集配置文件都长得很像。下面是一个标准的 data.yamlpath: ./datasets/traffic_sign train: images/train val: images/val names: 0: speed_limit_30 1: speed_limit_50 2: stop 3: no_entry 4: yield 5: keep_right这里有三个坑。第一是path字段必须写成绝对路径或者相对于运行命令的路径写成相对./datasets但在其他目录执行命令就会找不到数据。第二是train和val指向的是图片目录标签目录框架会自动去同名目录找所以图片目录叫images标签目录就必须叫labels不能改。第三是names的索引必须和标签文件里的类别 id 完全一致写错一个数字训练出来整个类别全是错的。3.3 对比实验结果为了对比公平三个版本我都用了相同的输入尺寸 640相同的 batch size 16相同的训练轮数 100 轮。我用的显卡是单张消费级显卡显存有限所以直接选择模型的轻量版本作为对比基准。实际跑下来的结果如下。版本参数量mAP0.5mAP0.5:0.95推理耗时(ms)显存占用(MB)v5 轻量7.2M0.9120.67314.26100v8 轻量11.2M0.9350.70112.87200v10 轻量8.4M0.9410.71211.56900从表格能看到v10 在精度和速度上都占了优势但训练稳定性和文档完整度不如 v5。如果目标是快速上线、遇到问题能搜到解决方案v5 仍是不错的保底选择。如果想追求性能极限并且有时间排查框架本身的 bugv10 值得投入。v8 则是最均衡的它在精度上接近 v10生态和文档又比 v10 更成熟我最终的项目选型也落到了 v8。4. 实操过程与关键环节理论说太多没用直接看实际操作。4.1 数据集目录组织我建议所有实验之前先把目录结构固定好不要东一个文件夹西一个文件夹。datasets/ traffic_sign/ images/ train/ val/ labels/ train/ val/ data.yaml训练集和验证集按 9:1 划分。这里有个小经验划分之前先用脚本统计每个类别的样本量确保验证集里每个类别至少有两个样本。如果某个类别非常稀少宁可让它多出现在验证集里也不要让它只出现在训练集里。因为验证集里没有这个类别训练完根本无法知道模型对这个类到底学到了什么程度。4.2 训练命令与参数调节训练命令在不同版本里略有区别核心参数逻辑是一样的。以 v8 为例命令如下。yolo detect train \ datadatasets/traffic_sign/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/traffic_sign \ namev8_base这里我重点说明三个参数。第一个是lr0学习率设置非常关键。transport标志数据比较规整用小学习率稳定收敛效果更好我试过把lr0拉到 0.05前几个 epoch loss 震荡特别剧烈后期 mAP 也不如 0.01 的结果。第二个是patience它控制早停轮数。如果验证集连续很多轮不提升就自动停止能省下不少时间。但 patience 不能设太小我第一次设成 5结果模型还在热身阶段就被停了后面改成 20 才稳定。第三个是batch这个受显存制约。显存在训练过程中会有波动峰值可能比刚启动时高很多我一开始设成 32 直接爆显存改成 16 才安全所以设置时宁可保守一些。4.3 训练过程观察要点训练过程中不要只盯着 loss 曲线看还要关注输出的日志信息。这里有三个关键信号。第一是box_loss和cls_loss的下降趋势。正常情况下两个 loss 应该在训练初期快速下降然后进入平台期。如果box_loss降下去了而cls_loss纹丝不动说明模型在学“框在哪里”而不是“是什么”这时候要检查类别映射是否错误、数据是否不均衡。第二是验证集 mAP 是否有周期性跳变。如果你看到 mAP 曲线突然掉下去又突然涨回来大概率是数据增强太强导致个别 batch 变成了“假样本”可以把增强参数调弱一些。第三是P和R的关系。如果 Precision 很高但 Recall 很低说明模型只捡容易检的漏检了大量小目标或遮挡目标要去查置信度阈值设置或特征提取层是否太浅。4.4 模型导出与验证训练完成后能导出为 ONNX 格式非常重要。这一步的意义在于它可以让模型脱离训练环境跑在通用的推理引擎上也能验证模型本身有没有对训练框架产生隐性依赖。yolo export modelruns/traffic_sign/v8_base/weights/best.pt \ formatonnx \ imgsz640 \ dynamicTrue导出后在 ONNX 推理引擎里做一次精度对齐测试。我遇到过一种情况导出后单张图片推理结果和 PyTorch 直接推理结果不一致后来排查发现是模型里用了某些依赖输入尺寸的算子导出时固定了尺寸推理时输入尺寸不同就出现偏差。用dynamicTrue可以避免大部分问题但会用掉更多推理时间。如果部署端固定输入尺寸建议直接导出固定尺寸的模型最稳妥。5. 常见问题与排查实录整个流程走下来我积累了一份问题速查表记录的都是容易困扰新手的实际问题后面做同类项目可以直接对号入座。现象可能原因解决方案训练 loss 不下降标签文件路径错误模型学不到真实目标检查 labels 目录是否存在用脚本可视化标注框mAP 始终很低类别 id 映射错误随机抽一张训练图画框验证类别是否正确训练时显存溢出batch 过大或图片尺寸过大调小 batch 或 imgsz开启梯度累积小目标大量漏检输入分辨率太低提升 imgsz 到 960或在增强里减小马赛克拼图块数推理时检测框偏移归一化坐标精度不足标签坐标保留至少 6 位小数夜间场景全检不出训练数据缺少夜间样本做亮度扰动或者额外补充夜间标注数据验证集比训练集指标高很多数据集划分随机性导致过拟合检查验证集是否包含重复帧重新划分再说一个我踩过比较深的坑训练过程中loss正常下降、mAP 也正常但实际用录制的车载视频去测发现连续帧之间检测框抖动非常明显。原因是训练时数据增强里的随机变化让模型学到了帧间不稳定性而视频推理时每帧独立检测导致同一目标在连续帧里框的位置和大小有轻微抖动。解决方式是推理端做轻量级的时序平滑对同一个目标的连续检测框计算加权均值比单纯去改模型要快得多。还有一个容易被忽视的是数据泄露问题。C 数据集是按视频抽帧的同一段视频里的相邻帧在画面内容上高度相似。如果训练集和验证集是从同一段视频里抽的验证指标会虚高模型看起来很强一换实拍场景就露馅。正确做法是按视频片段划分而不是按单帧划分比如第一段视频的前 80% 帧进训练集后 20% 帧进验证集这个思路在构建自己的数据集时一定要留意。我个人在实际操作中比较建议的做法是第一轮用轻量版模型快速验证数据和训练配置没问题再换大体量版模型跑最终效果。这样可以避免在配置错误的情况下空耗几个小时的训练时间。另外交通标志识别虽然看起来简单但真实道路中的复杂光线、季节变化、标志褪色都会带来数据分布漂移训练阶段多加入光照扰动和模糊模拟比后期反复调参的效果来得更直接。这个项目后续如果继续扩展我会把重点放在多类别标志的歧义处理上比如“禁止掉头”和“禁止左转”在远距离时视觉特征很像这一步做不好mAP 再高都是表面数据。
阅读完成 · 觉得有帮助?
咨询建站