干这一行的人应该都有同感:交通标志检测是目标检测所有场景里“看起来简单、做起来折磨”的典型。标志尺寸小、类别多且相似度高、户外光照随时在变,再加上雨雪遮挡和运动模糊,一套模型要在车端那点算力上跑得又准又快,真不是随便跑几个epoch就能交差的。YOLOv5到现在依然是性价比最高的选择之一,尤其是配合一套干净的数据集和合理的源码改动,完全可以在可控成本内做出可靠的识别效果。这篇东西我会把自己实际用YOLOv5做交通标志检测与识别的完整套路摊开讲讲,从数据集的选型清洗、标注格式转换、模型配置与训练调参,到推理阶段的精度问题和边缘端部署优化,每一步都有操作细节和踩坑记录,适合正在入门目标检测的学生,也适合已经把YOLOv5跑通但准备往实际项目上推的开发者参考。
1. 交通标志检测的难点,为什么偏偏是YOLOv5能顶住
交通标志检测和通用物体检测有本质区别。通用检测里“人”“车”“猫”“狗”这类目标尺寸相对稳定,特征冗余度高;交通标志则是典型的极小目标,在一个1920×1080的画面里,一个限速标志可能只有30×30像素甚至更小,占整张图不到0.1%的面积。而且标志类别之间差异非常细:限速40和限速50的区别就是几个像素的数字,圆形红框和圆形蓝框的分界线也非常明确。这些问题叠加在一起,对小目标特征提取能力、分类边界敏感度和实时性要求都极其苛刻。
传统方案和早期深度学习方案各有各的死穴。基于颜色分割加形状检测的老办法,遇到逆光、阴影、褪色标志基本就废了;两步检测器(旧版Faster R-CNN那一路)精度倒是能看,但一张图跑几十毫秒到上百毫秒,视频流处理根本扛不住。YOLOv5能在中间找到一个很合适的平衡点:它把检测当成回归任务一步到位,没有独立的候选框生成阶段,速度上的优势非常明显——在GTX 1080Ti上跑640分辨率输入,YOLOv5s的推理速度能到8毫秒左右,完全满足实时需求;而它通过多层特征融合(PANet结构)保留小目标的浅层特征响应,再配合可调anchor机制,让交通标志这类小目标有了被“看见”的通道,这一点在项目实测中极其重要。
具体到YOLOv5的源码架构上,它对交通标志场景有三个天然友好的设计:
- Mosaic数据增强:把四张图拼成一张训练,相当于把画面里的小目标数量成倍放大,对标志这种天然稀疏的目标非常有效。
- 自适应anchor计算:训练前会自动统计数据集中真实框的宽高分布并重新聚类计算anchor,不需要背着默认参数硬上。
- 多尺度训练与推理:训练时随机切换不同输入尺寸,推理时可按需选择640、960甚至1280,小目标场景可以靠“上分辨率”换取召回。
当然,这几个优势能不能全部兑现,关键还看数据集和训练策略。接下来我详细拆数据这块,因为这是整个项目里最脏最累但性价比最高的一环。
2. 数据集的选型、清洗与标注处理实战
交通标志的实际样本来源就三条路:公开数据、自采数据、合成数据。我建议的路线是“公开为主、自采为辅、合成为补充”,前提是你先把公开数据集里的坑摸清楚。
2.1 公开数据集怎么选,各有什么坑
市面上能直接下载到的交通标志数据集不少,但质量参差不齐。我根据自己做项目的经验做了个横向对比,方便你判断该用哪个:
| 数据集特征 | 数据A(国内路况) | 数据B(欧洲街景) | 数据C(城市路口) |
|---|---|---|---|
| 图片数量 | 上万张,涵盖高速与城市 | 数千张,场景相对单一 | 数千张,偏郊区道路 |
| 标注类别 | 数十类,含限速/禁令/警告等 | 数十类,类别体系完善 | 类别较少,但清晰度高 |
| 小目标占比 | 中等,大量远距离标志 | 高,多数标志像素极小 | 偏低,采集距离较近 |
| 标注精度 | 部分框偏移严重 | 规整,但存在重复框 | 较规整 |
| 适合用途 | 国内落地项目首选 | 算法验证与小目标专项 | 边缘端快速白盒验证 |
数据A的问题是标注质量不稳定,有些大图的标志框框得松松垮垮,尤其是模糊的远距离标志;数据B的标注相对规整,但欧洲路况和国内的路牌风格差异大,直接拿来当主训练集会水土不服。数据C适合做快速验证,但类别少,不是生产级选择。
我实测的结论是:主力用数据A,把数据B里的小目标图片抽一部分当补充,数据C可以用来快速搭建demo验证改动是否有效。三者切换时要特别注意类别的对齐映射,不同数据集里同类标志的类别ID可能不一样,稍不留神就是错乱。
2.2 标注格式统一与清洗,代码直接拿走
下载下来的公开数据集格式五花八门:有的是VOC XML,有的是COCO JSON,有的干脆是自己的一套txt。YOLOv5需要的是每张图对应一个同名txt,每行内容为“类别 x_center y_center width height”,中心点坐标和宽高都是相对图像宽高的归一化浮点数。这里每一步都要小心:归一化坐标算错或者图片读取尺寸不一致,模型训练出来就直接废了。
我通常用一个脚本来完成从VOC XML或标注表到YOLO格式的批处理。核心逻辑是遍历所有XML,解析object节点里的bndbox坐标,再除以图片宽高得到归一化值。关键点是解析前先确认图片实际尺寸,很多数据集的XML里没有width/height字段,而直接读取图片又可能因为EXIF旋转导致宽高颠倒。稳妥做法是优先从图片文件本身读取宽高,而不是依赖XML自带的size字段。
清洗环节往往被新手忽略,但这步对模型上限的提升比调参还大。我会按这三条规则过一遍数据:
- 去掉目标框面积小于原图0.001%且又模糊不可辨别的样本,这类样本对训练几乎无益,还会把模型带偏。
- 去掉标注框与标志实际轮廓偏差严重的样本,例如框内包含了大面积背景,或框只覆盖了一部分标志。
- 去掉类别标错的样本,这个基本只能靠人工抽检,优先检查数量最多的那几个类别,比如限速和禁止标志往往容易混。
清洗一遍之后,样本数可能从一万张降到八千张,但训练出的模型mAP通常会涨2~4个点,效果立竿见影。
2.3 数据增强策略,哪些适合交通标志
YOLOv5自带增强开关在hyp.scratch-low.yaml里,默认已经开了Mosaic、随机仿射、HSV扰动等。针对交通标志场景,我会额外做两个调整:
- 加大旋转和透视范围:车辆行驶中看到的标志往往有侧倾或透视变形,把旋转范围从默认的10度提高到20度,透视参数从0.0调整到0.2,模拟弯道和侧方观察。
- 加高斯模糊和噪声:对应雨天、尘土、传感器噪声场景。在
albumentations之类的外部管线里,对输入图像以一定概率叠加轻度模糊和椒盐噪声,注意别加太重,否则会破坏标志本身的纹理。 - 限制过度的颜色扰动:标志的颜色是强语义特征,HSV saturation的默认范围可以保留,但hue的偏移量最好压低,避免蓝色标志被偏成紫色,导致模型学偏。
这里有个容易被忽略的原则:增强是让你在不增加标注成本的情况下复现真实场景分布,而不是把图片改得不像真实世界。交通标志检测的高频失败场景是逆光和远距离,所以相比颜色剧烈变化,我更推荐在亮度对比度上做文章,并对整图的随机裁剪保持克制。
3. 基于YOLOv5的配置修改、源码定制与训练调参
数据就绪之后,进入模型侧的工作。这个阶段的核心不是跑通训练那么浅,而是让YOLOv5的配置真正贴合交通标志的小目标分布和类别特点。
3.1 模型版本选型与配置文件改动
YOLOv5有n/s/m/l/x五个规格,从轻到重分别是参数量和精度的递进。交通标志检测通常部署在车载设备或边缘盒子,我推荐把YOLOv5s作为起点,参数量大约7.2M,处理640×640检测速度极快,精度也够用;如果你跑下来召回率不达标,再考虑升级到m,而不是一上来就上l或x。边缘端推理最怕模型复杂化之后反而拖垮帧率。
下载源码之后,需要改三个核心文件:数据配置yaml、模型结构yaml、训练参数。
数据配置yaml(比如新建traffic_sign.yaml)内容大概长这样:
train: ./dataset/images/train val: ./dataset/images/val nc: 32 names: ['speed_limit_30', 'speed_limit_40', 'speed_limit_50', ...]这里nc要严格改成你的类别数,names列表顺序必须和标注txt里的类别ID完全对应。最常见的报错就出现在这里:标注里类别ID是1~N,但names列表从0开始数,导致训练时标签越界。
模型结构yaml的改动则更有讲究。我把YOLOv5s.yaml里的nc改成自己的类别数,同时把检测器的细粒度特征层往里加了一层。默认配置是检测P3、P4、P5三个尺度的特征层,P3对应小目标,但交通标志很多比P3的锚定尺度还要小,所以我在源码里把P2层也接入检测头,让8倍下采样的大特征图参与预测。这个改动在源码中不复杂,代价是训练显存增加20%左右,但对小目标的召回率提升明显。
另外,anchor不是必须手动的。YOLOv5训练启动时会在CheckAnchors阶段自动计算数据集的anchor,但如果你在yaml里预先指定了一组与数据分布更接近的anchor,训练收敛会更快。具体做法是先用k-means对训练集所有真实框做聚类,得到9个宽高比组合,然后填到模型yaml的anchors字段里。我建议初始anchor可以设置得更小,因为交通标志的目标比例整体偏小。
3.2 训练命令与关键超参数的实际取舍
环境配置部分直接过:Python 3.8+、PyTorch 1.10+,安装requirements.txt里的依赖即可。训练命令通常长这样:
python train.py --data traffic_sign.yaml --cfg models/yolov5s_traffic.yaml --weights yolov5s.pt --batch-size 16 --epochs 300 --imgsz 640 --device 0 --multi-scale几个超参数单独说一下:
--multi-scale建议打开,训练时输入尺寸会在0.5~1.5倍之间随机变化,对增强模型对不同距离标志的适应力很有帮助,代价是每个batch的耗时上升。--batch-size取决于显存,但要注意多尺度模式下显存波动,别把显卡塞满,否则训练途中容易出现OOM中止。--epochs不要迷信300这个数,我建议选一个较大值,同时配合早停。YOLOv5自带早停,当验证集mAP连续多轮(默认100轮)不提升时自动停止,避免无效训练时间。
训练过程中重点关注两个实时指标:验证集的mAP@0.5和Recall。交通标志这种小目标场景,precision可能很高但recall拉胯,那说明模型“保守过头”,宁可漏检也不误检,这对实际应用来说往往不够。我会在训练日志里同时盯这两个值,如果recall低于0.8,基本判定需要调整数据分布或增强策略。
3.3 源码级定制:针对交通标志的NMS与后处理改造
这部分算是我个人经验里比较值钱的一段。默认NMS在候选框重叠时通过IOU阈值去重,对普通目标没问题,但交通标志存在同类目标相邻且重叠的情况,比如两个限速标志并排出现在一个区域内。如果IOU阈值设得偏高,两个框可能被当成一个框合并掉,直接丢失一个目标。
我常用的处理是把NMS替换为DIoU-NMS或Soft-NMS。YOLOv5源码的general.py里有现成的NMS函数,我通过增加一个简单的掩码变化实现Soft版本:不再直接把重叠超过阈值的框置信度清零,而是按重叠程度衰减其置信度。这样两个并排标志都能保留,只是置信度稍微下降。实测在标志密集的路口场景,召回率能提升2~3个点。
另外一个定制是置信度阈值的动态机制。在低阈值下,模型会吐出一堆低置信度的框,包含大量误检;但直接调高阈值又会把难例的正确检测一起滤掉。我最后采用的做法是:在推理代码里分别设置conf_thres用于常规目标,对面积小于一定像素且类别属于限速、禁令的框使用更低的conf_thres,因为小目标本身的置信度天然偏低,这一步是纯源码层的微调,效果却很直接。
4. 推理部署、精度评估与边缘端优化的完整链路
训练的终点是落地。这个阶段需要同时处理模型的格式转换、速度优化和预估精度与实测精度的落差,每一环都有不少门道。
4.1 模型导出与加速
PyTorch模型不能直接拿到生产环境跑,我通常走“PyTorch → ONNX → TensorRT”的路线,在支持GPU的边缘设备上可以获得非常好的加速收益。
python export.py --weights runs/train/exp/weights/best.pt --include onnx --dynamic --simplify python export.py --weights best.onnx --include engine --device 0 --half第一段导出ONNX时,我建议打开--dynamic动态输入尺寸,方便后续对不同分辨率做推理;--simplify用onnx-simplifier清理掉冗余算子。第二段从ONNX转TensorRT的engine文件时打开--half,半精度浮点在小数点精度影响可忽略的同时,推理速度往往能翻一倍。实测在边缘设备上,YOLOv5s的TensorRT FP16推理速度能到30~40FPS,中等算力足够支撑实时检测。
如果设备不支持TensorRT,那就直接用ONNX Runtime跑。这种情况下要注意输入尺寸固定,避免动态维度带来的额外耗时。有一个小细节:TensorRT的batch size最小化要设置成1或4,否则显存分配会偏大,影响其他任务。
4.2 精度评估和常见失败样本排摸
模型练完别急着上线,先拿一组留出测试集跑一轮完整的mAP评估,还要重点挑出失败样本逐个分析。我发现交通标志检测的失败模式高度集中:
| 失败场景 | 典型原因 | 常用处理手段 |
|---|---|---|
| 远距离小标志漏检 | 特征图分辨率不足 | 增大输入分辨率,接入P2检测层 |
| 逆光/阴影下漏检 | 样本分布缺失 | 增加亮度扰动,采集逆光样本 |
| 广告牌/圆形标识误检 | 背景语义相似 | 增加负样本,调整置信度阈值 |
| 多个标志并列漏检 | NMS合并 | 替换Soft-NMS / DIoU-NMS |
| 模糊/雨天漏检 | 纹理信息缺失 | 轻微模糊增强,降低conf_thres |
针对这些失败样本,最笨但最有效的方法就是回到数据层:把失败样本归到一个“补充集中”,人工标注后并入训练集,再迭代一轮,通常效果立竿见影。这比反复调超参数要靠谱得多。
4.3 工程化部署中的帧处理与状态平滑
真正放到视频流场景里,单帧检测的不稳定性会被放大。某一帧漏检了,下一帧又检测到,结果看起来就是“闪烁”的识别框,非常影响使用体验。我在工程里加了两个简单机制来平滑结果:
- 帧间目标跟踪:利用目标框的中心点位移做轻量级关联,连续多帧出现在同一位置的标志即使某一两帧漏检,也能保持框的延续显示。
- 置信度滑动平均:对同一个跟踪目标的置信度取多帧均值,只有均值低于阈值时才撤销框,避免单帧抖动导致的误判。
这些逻辑放在业务代码层面即可,不用改模型结构。它们对真实场景体验的提升比模型再涨两个点mAP还明显。
硬件侧,边缘设备的功耗和散热也是实际要考的。跑TensorRT推理时GPU占用可能不低,长时间运行要注意设备散热。我在车载验证时遇到过设备因高温降频导致帧率骤降的问题,后来在代码里加了简单的推理频率控制——检测帧率和视频帧率解耦,检测可以按每2帧或3帧跑一次,中间帧用上一帧结果插值,既保平滑又减负载。
我在这条路上最大的体会是:YOLOv5做交通标志检测,70%的成败在数据,20%在配置和训练细节,只有10%在模型结构本身。与其不断换新模型追指标,不如先把手头的数据搞干净、把推理链路做扎实。很多新手盯着mAP看,而真正上线的时候,决定项目生死的往往是NMS那一个框的取舍和边缘设备上那几瓦功耗的控制。最后再分享一个实用小技巧:训练完成后,把results.png里那张PR曲线图放大看,下限速和禁止标志那一类,如果曲线尾段下降特别陡,说明置信度阈值必须单独调,别指望一个统一阈值吃遍所有类别。希望这篇内容能让你在这个项目上少走几圈弯路。