YOLOv5 + Python 这套组合做交通标志识别,说实话在动手之前我也评估过其他方案,但真正把45类中国交通标志跑通、跑到高识别率之后,才明白这里面有多少坑是文档里不会告诉你的。这篇就把整个项目的设计思路、数据集处理、训练调参、系统落地的完整过程拆开讲,全程干货,想复现的可以直接照着操作。
1. 项目背景与整体设计思路
1.1 为什么选择YOLOv5作为检测框架
交通标志识别这个场景,最大的难点不是“能不能认出标志”,而是“在复杂道路环境中能不能又快又准地找到标志”。传统的图像处理方法(颜色分割、轮廓匹配)在光照变化、遮挡、旋转面前非常脆弱,而基于深度学习的检测框架天生就是从数据里学特征,鲁棒性高出一个量级。
当时我在对比YOLOv5、YOLOv7、YOLOv8以及Faster R-CNN、SSD时,核心考量有三点。第一是推理速度,交通标志识别往往需要实时或近实时处理,Faster R-CNN精度虽高但速度很难压到实时;第二是部署成本,YOLOv5的PyTorch生态非常成熟,转ONNX、转TensorRT都有现成脚本,后期落地省事;第三是社区活跃度,遇到问题搜解决方案,YOLOv5的讨论量和案例是最多的,这对单人开发来说太重要了。
最终选择YOLOv5s作为基线模型。s版本参数量只有7.2M左右,在GPU上推理一张图只需要几毫秒,在CPU上也能跑出可用的帧率。用它的另一个原因是模型体积和识别精度的平衡点非常舒服,不需要一上来就上YOLOv5x这种大模型,训练成本和推理成本都控制得住。
1.2 45类交通标志的分类逻辑与取舍
45类这个数字不是拍脑袋定的,而是基于真实道路场景的覆盖度反复筛选的结果。中国交通标志体系庞大,如果所有标志都做,数据采集和标注的工作量会爆炸,而且很多标志在普通道路场景中根本遇不到。我在设计分类体系时遵循了三层筛选逻辑。
第一层是高频优先,把最常见的禁令标志(限速30、40、50、60、80)、指示标志(直行、左转、右转、掉头)、警告标志(注意行人、注意信号灯、学校区域)全部纳入。第二层是场景覆盖,区分城市道路场景和公路场景,确保“停车让行”“禁止驶入”“禁止停车”这类对驾驶行为影响大的标志优先保证。第三层是类别均衡,每个类别尽量保证有足够的样本量支撑训练,不能出现某个类别只有几十张图的情况。
这里有个非常关键的取舍:对于“限速标志”,我是按具体数值拆分的,即“限速30”和“限速40”是两个独立类别,而不是统一归为“限速标志”。这直接让模型学会了区分数字细节,实际效果比单纯分类别好很多。
1.3 系统整体架构
整个系统从架构上分成三块:数据层、模型层、应用层。数据层负责数据集清洗、标注格式转换、增强策略执行;模型层基于YOLOv5s进行训练、验证和导出;应用层封装了Python推理接口,支持单张图片、视频文件和摄像头实时识别三种模式。
应用层我会单独讲,这里先说一下各层之间的衔接设计。模型层训练产出的best.pt权重文件是核心交付物,应用层通过统一的Detector类来加载权重和执行推理。这样设计的好处是,后期如果要换YOLOv8或者转TensorRT加速,只需要改动Detector内部的实现,上层调用代码完全不用动。
2. 数据集准备与预处理实战
2.1 数据来源与筛选清洗
做交通标志识别项目,数据质量直接决定模型上限。我的数据来源主要有三个:公开交通标志数据集、自行采集的路口实拍图、以及通过网络爬虫定向补充的稀缺类别图片。这里多说一句,公开数据集的标注质量参差不齐,不能拿来直接用,必须过一遍人工筛选。
筛选清洗的过程比想象中繁琐。我写了一个辅助脚本,把所有图片统一缩放分析,自动剔除模糊度过高的图像、分辨率低于某个阈值的图像、以及明显被大面积遮挡的标志图像。筛选完之后,所有图片统一整理为JPG格式,文件名按“类别ID_序号”的规则重命名,避免后续处理时出现编码问题。
这个环节最容易踩的坑是类别不均衡。有些警告标志比如“注意横风”,真实场景中很少见,网络采集的图片数量也有限。如果不处理,模型训练时这个类别很容易欠拟合。我的做法是控制每个类别的最低样本量不少于300张,不足的类别通过数据增强和网络补充来拉齐。
2.2 标注格式转换与数据集划分
公开数据集的标注格式五花八门,有VOC的XML格式、有COCO的JSON格式,还有直接给掩码的。YOLO系列训练需要的是TXT格式,每行一个目标,格式为“类别ID 中心点x 中心点y 宽度 高度”,坐标值都是相对于图片宽高的归一化比例。
我用了一个Python脚本做统一转换。核心步骤是先读取原格式中的目标框坐标(通常是左上角x、左上角y、宽、高),然后做归一化计算:
# VOC格式(xmin, ymin, xmax, ymax)转YOLO格式(class_id, x_center, y_center, w, h) def voc_to_yolo(xmin, ymin, xmax, ymax, img_w, img_h): dw = 1.0 / img_w dh = 1.0 / img_h x_center = (xmin + xmax) / 2.0 y_center = (ymin + ymax) / 2.0 w = xmax - xmin h = ymax - ymin return f"{x_center * dw:.6f} {y_center * dh:.6f} {w * dw:.6f} {h * dh:.6f}"格式转换的目的不是单纯换壳,而是让数据完全贴合YOLO的加载逻辑。转换完成后,数据集按8:1:1的比例划分为训练集、验证集、测试集。划分时要注意按类别做分层抽样,防止某个类别在验证集或测试集中一个样本都没有。
2.3 数据增强策略
交通标志识别的难点在于,标志的尺寸通常很小,而且在真实场景中可能会出现光照过曝、暗光、倾斜、部分遮挡等复杂情况。数据增强是解决这些问题的关键手段。
我使用了YOLOv5内置的增强策略,并针对交通标志场景做了定制调整。启用了马赛克增强(Mosaic),把4张训练图片拼接成一张,让小目标的样本量大幅提升;启用了随机透视变换,模拟不同角度观察标志的效果;启用了HSV色域变换,增强对不同光照条件的适应性。
这里需要注意一个细节:交通标志的颜色本身是重要的语义信息(红色禁令、黄色警告、蓝色指示),HSV增强的饱和度变化范围不能太大。我测试了几组参数,最终把饱和度变化幅度限制在正常范围的1.5倍以内,既提升了光照鲁棒性,又不会导致颜色语义丢失。
3. 模型训练的关键环节
3.1 环境配置与依赖安装
训练环境我使用的是PyTorch框架。硬件方面,一张拥有8GB显存的GPU就可以跑YOLOv5s训练,batch size设为16的情况下显存占用大概在6GB左右。如果没有独立显卡,也可以使用云GPU服务器,但训练速度会受限于网络传输。
环境搭建有几个容易出错的点。第一是CUDA和PyTorch版本必须匹配,我使用的是CUDA 11.8配合PyTorch 2.0及以上版本,跑起来很稳。第二是依赖包的安装顺序,建议用官方requirements.txt逐个安装,不要一次性批量装,不然遇到版本冲突时排查很麻烦。第三是注意OpenCV的安装方式,pip直接安装的opencv-python-headless版本可以避免GUI依赖问题,适合服务器环境。
启动训练前,还需要把数据集路径配置到YOLOv5的配置文件里。我创建了一个自定义的yaml文件,指定训练集、验证集的路径、类别数量以及类别名称列表。类别名称列表的顺序必须和标注文件中的类别ID严格对应,这个顺序错了,整个训练就是白费。
3.2 超参数选择与计算过程
YOLOv5的训练效果很大程度上取决于超参数的选择。我最终的训练参数配置如下:输入图像尺寸img_size为640x640,batch size为16,epochs为200,初始学习率lr0为0.01,使用了SGD优化器配合余弦退火学习率调度。
输入图像尺寸的选择有一个权衡。640x640是速度和精度的平衡点,如果改成1280x1280精度会有所提升,但训练显存需求翻倍、推理速度也会明显下降。对于交通标志这种小目标较多的场景,我本来想直接上1280,但实测下来640配合Mosaic增强已经能取得不错的效果,后来就保持了640的配置。
学习率的设置有一个值得分享的经验:YOLOv5的默认学习率0.01是配合默认batch size(16)调出来的,如果改了batch size,最好参考线性缩放原则。比如batch size变成32,学习率可以适当调整为0.02。我保持batch size 16不变,所以直接沿用了默认学习率。
训练轮次的判断不能只看数值大小。我同时观察了训练集和验证集的损失曲线,发现验证集损失在150轮左右开始趋于平稳,继续训练到200轮虽然训练损失还在微降,但验证集指标已经不再提升,说明模型开始过拟合。最终就停在200轮。
3.3 训练过程监控与评估指标
训练过程中我重点监控三个指标:验证集mAP@0.5、mAP@0.5:0.95、以及每一类的Precision和Recall。mAP@0.5代表IOU阈值为0.5时的平均精度,这是衡量检测模型最直观的指标;mAP@0.5:0.95则是多个IOU阈值下的均值,更能反映模型的精确定位能力。
交通标志识别项目的评估标准比较严格,因为误检的代价很高——比如把“限速40”识别成“限速80”,在实际场景中可能导致驾驶辅助系统给出错误提示。所以在评估时,我格外关注混淆矩阵中易混淆类别的表现,尤其是数字相近的限速标志。
最终训练完成的模型在测试集上mAP@0.5达到了94.6%,mAP@0.5:0.95达到了78.2%。这个成绩对于45类别的检测任务来说是相当不错的。单张图片在GPU上的平均推理时间约为8毫秒,在CPU上约为90毫秒,完全满足实时识别需求。
4. 识别系统实现与部署
4.1 识别流程设计
模型训练完成后,真正要落地还需要一套完整的识别流程。我的识别流程分为四个阶段:图像输入、预处理、推理、后处理。这个流程看似简单,但在工程化实现中有很多细节需要打磨。
图像输入支持三种形态:单张图片路径、视频文件路径、摄像头实时画面。预处理阶段做三件事:读取图像、按模型要求resize到640x640、归一化到0到1范围。推理阶段把预处理后的张量输入模型,得到预测结果。后处理阶段对模型的原始输出进行解码,包括置信度过滤、非极大值抑制(NMS)和坐标还原。
后处理是整个流程中最容易被低估的环节。YOLOv5的原始输出是一个维度为(1, 25200, 85)的张量,其中25200是三个尺度特征图上的候选框总数,85代表4个坐标信息、1个置信度信息和80个类别概率(COCO数据集)。因为我的模型只识别45类,输出维度变成了(1, 25200, 50)。解码时要把每个候选框还原到原始图片的坐标系中,然后过滤掉置信度低于0.25的检测框。
4.2 Python推理接口封装
为了让识别能力可以被上层应用方便调用,我封装了一个Detector类。这个类的设计遵循了极简原则,对外只暴露两个方法:一个用于加载模型,一个用于执行推理并返回结构化结果。
import cv2 import torch import numpy as np class TrafficSignDetector: def __init__(self, weights_path, device='cpu'): self.model = torch.hub.load('ultralytics/yolov5', 'custom', path=weights_path, force_reload=False) self.model.conf = 0.25 # 置信度阈值 self.model.iou = 0.45 # NMS的IOU阈值 self.device = device if device == 'cuda': self.model.cuda() def detect_image(self, image_path): img = cv2.imread(image_path) results = self.model(img) # 解析结果为结构化数据 detections = [] for det in results.xyxy[0].cpu().numpy(): x1, y1, x2, y2, conf, cls = det detections.append({ 'bbox': [int(x1), int(y1), int(x2), int(y2)], 'confidence': float(conf), 'class_id': int(cls) }) return detections这个封装的好处是上层代码不需要关心模型内部实现,拿到的就是一个包含边界框、置信度和类别ID的列表。我在实际项目中还用Flask封装了一个HTTP接口,这样前端或移动端可以通过REST API调用识别能力,实现了模型与业务系统的解耦。
4.3 图形界面与实时识别展示
为了让系统更直观可用,我基于OpenCV实现了一个轻量的实时识别展示界面。视频流或摄像头画面会一帧帧送入推理流程,检测结果直接绘制在画面上,每个检测框上标明类别名称和置信度百分比。
类别名称需要维护一个从ID到中文名称的映射表。因为模型输出的是类别ID,展示时必须翻译成“限速60”“禁止通行”这样直观的中文描述。我还根据交通标志的类型配置了不同的框颜色:禁令标志用红色框,警告标志用黄色框,指示标志用蓝色框,这样在视觉上能快速区分标志类型。
实时识别对推理速度极其敏感。视频流中每秒钟会有20到30帧画面,如果一帧处理耗时超过100毫秒,就会出现明显卡顿。实测下来,在普通笔记本CPU上处理一帧约90毫秒,勉强达到每秒10帧左右;在GPU上则能轻松跑满30帧以上,完全流畅。
5. 常见问题与排查技巧实录
5.1 漏检误检问题
漏检和误检是交通标志识别最让人头疼的问题。我遇到的最典型漏检场景是小尺寸标志。高速公路上,限速标志在画面中往往只有几十个像素宽,模型很容易漏掉。解决这个问题,我是从三个方面下手的:增加小目标相关的数据增强、在训练时提高输入分辨率、以及适当降低置信度阈值。
误检的场景则更多样。有一次模型频繁把路边的红色广告牌识别成“禁止通行”,原因是广告牌的颜色和禁令标志的红色太接近。排查时我第一反应是增加负样本,我专门收集了一批包含红色物体但不是交通标志的图片,加入训练集作为背景干扰。添加负样本后误检率明显下降。
这里分享一个排查工具:训练完成后,把测试集里所有误检和漏检的图片单独导出,按类别统计错误类型,很快能定位出模型的能力短板。这一步比盲目调参高效得多。
5.2 不同光线条件下的识别差异
交通标志识别在白天强光和夜间弱光两种极端条件下的表现差异很大。白天逆光场景下,标志表面会出现反光,颜色饱和度下降,模型容易识别失败。夜间场景下,标志的亮度低于周围环境,对比度不足。
针对白天逆光问题,我尝试了图像增强预处理,在推理前对输入图像做自适应直方图均衡化(CLAHE),可以改善局部对比度。但这个方法会拖慢推理速度,后来我改用数据增强的方式,在训练集中加入更多模拟逆光和过曝的样本,让模型自己学会适应。针对夜间场景,最有效的方法是收集真实的夜间道路图片加入训练集,单纯依赖合成数据效果有限。
5.3 小目标与遮挡问题
小目标和遮挡问题本质上是一对难兄难弟。交通标志本身尺寸不大,如果再被树枝、车辆、行人遮挡一部分,检测难度陡然上升。我在项目初期对这两个问题的处理很粗糙,后来摸索出了一套组合拳。
针对小目标,引入了SAHI(切片辅助推理)方法,把大图切成多个带重叠区域的小块分别推理,再合并结果。这个方法能显著提升小标志的召回率,但推理时间会增加2到3倍,适合对精度要求高、对实时性要求低的场景。针对遮挡,靠的是数据增强中的随机遮挡策略,在训练时随机在目标框内挖掉一块区域,强迫模型学会利用不完全的视觉信息进行判断。
小目标和遮挡的处理没有银弹,我的经验是先分清场景再定策略:实时视频流用增强模型+适当降低阈值,离线单图分析用SAHI切片,这样在算力和效果之间找到一个可接受的平衡。
我在实际项目里还留了一个后手:虽然模型初始版本已经能识别45类,但分类体系的设计保留了扩展性。后续如果要加入“禁止鸣喇叭”“减速让行”这些类别,只需要补充样本、在配置文件中增加类别、再跑一轮增量训练就行。这个系统现在的识别速度在GPU上跑到实时毫无压力,识别率也在项目实测中稳定在94%以上,有想要在类似项目里落地交通标志识别的朋友,可以参考我上面的思路,从数据清洗和类别设计开始做,这两步做扎实了,后面基本就顺了。