简介:一份面向毕业设计、课程设计与期末大作业场景的道路交通标识识别系统,基于YOLOv5目标检测算法构建,提供完整的Python源码与配套数据集。项目代码注释详细,从数据加载、模型训练到推理可视化均有清晰说明,即使新手也能较快理解核心逻辑,适合自动化、计算机等相关专业的本科生作为高分毕设或课设参考。压缩包共266个文件,体积423.33MB,包括59个yaml配置文件、53个py源文件、61张jpg/jpeg图像样本、10个pt预训练权重,以及csv训练记录、sh部署脚本、ipynb演示笔记、Dockerfile等,能够支撑环境搭建、训练调参、效果展示全流程。目前已有50人学习下载,作者个人项目获评98分并获导师认可,部署方式简单,运行脚本与依赖说明齐备。此外训练日志中保存了TensorBoard事件文件,可直观查看精度变化,为答辩与项目复盘提供数据支撑。
1. 基于YOLOv5算法实现道路交通标识识别系统:这个毕设到底在解决什么问题
你在驾校科目一刷题时见过那种“限速40”的圆形红圈,在高速上导航提醒“前方有学校”靠的也是它。道路交通标识识别系统,就是把“图片里有没有标志、标志在哪个位置、它是什么类别”这三件事一次解决掉的目标检测任务,而基于YOLOv5算法做实现,是当前毕设场景里性价比最高的路径:它有能跑的Python源码、相对成熟的数据集生态,而且从数据准备到模型训练再到实时推理,每一步都能被拆成可验证的工程动作。这个方案适合两类人:一是拿它当毕设或课程设计、需要尽快跑通并讲清楚原理的学生,二是想入门YOLO系训练流程、后面打算做自动驾驶感知或辅助驾驶功能验证的从业者。核心门槛不在模型本身,而在怎么把数据、超参数和边界情况处理好,这也是下面要展开的全部内容。
2. 为什么选 YOLOv5 做交通标识识别:版本选型、网络结构与数据准备
2.1 检测算法选型:为什么不是 Faster R-CNN,也不是 YOLOv8
交通标识识别属于目标检测,不是图像分类。分类任务只需要回答“这里有没有限速牌”,而检测任务还必须输出标志的边界框坐标。这意味着算法选型时看的不只是准确率,还有回归框的稳定性和推理速度。Faster R-CNN 的精度在复杂场景下确实能打,但它是两阶段检测器,Region Proposal 阶段会拖慢速度,在答辩现场做实时摄像头演示时,帧率往往不理想。SSD 速度够但小目标检测一直是短板,而交通标识恰恰以中小目标居多——远处一块直径几十厘米的标志牌,在画面里可能只有二三十个像素,SSD 的多尺度特征融合方式很难接住这种目标。
YOLOv5 是当前毕设场景里最不容易卡壳的选择。它的源码组织清晰,训练、验证、导出脚本全是现成的,遇到问题能搜到大量同类讨论;它提供 s、m、l、x 四档模型尺寸,16G 显存以下跑 YOLOv5s 完全无压力;它的后处理逻辑已经封装在 detect.py 里,不需要自己手写 NMS。YOLOv8 的 anchor-free 设计确实带来了一部分 mAP 提升,但对交通标识这种强颜色、强几何结构的目标来说,提升幅度不会质变,反而因为生态文档不如 v5 厚,踩坑时能参考的现成经验少一截。这个赛道的项目大多是“用现成框架做垂直场景验证”,求的不是刷榜,而是可控、可讲、可演示。
在锚定 YOLOv5 后,还有一个隐藏选型:用官方源码还是第三方重构版。我一般建议直接用官方仓库的 YOLOv5 源码作为底座,因为第三方版本可能在预处理、anchor 计算或损失函数上做过改动,训练出来的权重未必能无缝接进你后面的部署流程。源码层面,官方版的训练入口是 train.py,推理入口是 detect.py,导出统一走 export.py,这三个文件就是整个系统的骨架。
2.2 看懂 YOLOv5 网络结构图:Backbone、Neck 和检测头对小目标的影响
很多人在调参时把网络当成黑匣子,但交通标识识别这个任务,结构理解直接决定了你该改哪个参数。YOLOv5 的网络结构图分三段:Backbone 负责提取特征,核心是 CBS(Conv+BN+SiLU)模块和 C3 模块;Neck 采用 FPN+PAN 结构,把高层语义特征向下传递、把低层纹理特征向上传递并做拼接;Head 部分输出三个尺度的预测结果,分别对应 stride 8、16、32 的检测层。小目标主要由 stride 8 的检测层负责,它保留的空间细节最完整,感受野也最小。
交通标识的难点恰好落在这个结构上:标志牌目标小,且大量是红蓝白这种高饱和度颜色,模型能不能在小尺寸特征图上分辨出“限速80”和“解除限速80”,取决于低层特征有没有被有效保留。理解这一点后,调参就有了方向。比如你把训练尺寸从 640 提到 960,相当于让输入给小目标分配更多像素,最直接的效果就是小目标召回率上涨;但随之而来的是显存占用变大,训练速度变慢,所以它是一个需要权衡的操作。再比如 mosaic 增强,它把四张图拼在一起训练,对小目标多的数据集很友好,但如果图片里存在大量被裁切到边缘的标志牌,mosaic 反而会生成大量“半截目标”,需要在超参数里关掉或者降权。
yolov5 网络结构图里还有一点容易被忽略:C3 模块的层数和通道数决定了模型的容量,但它不是越大越好。YOLOv5s 在 16G 显存、batch 16 的训练配置下,单轮时间大约在几十秒到几分钟量级,而换到 YOLOv5x 后显存和训练时间都会成倍上涨。做毕设的正确姿势是先用 s 尺寸把流程跑通,确认数据和评价指标没问题,再决定要不要升级模型尺寸。
2.3 把 TT100K / CCTSDB 转成 YOLO 标注:转换脚本与类别映射策略
数据是交通标识识别里决定上限的部分。公开数据集里,TT100K 和 CCTSDB 是最常被提到的两个,但它们的标注格式都不一样。TT100K 的标注是 JSON 格式,按图片逐张存放,每个对象的 bbox 字段给出了左上角和右下角坐标;CCTSDB 是 XML 格式,结构接近 PASCAL VOC。YOLOv5 要求标注是每张图片对应一个同名 TXT 文件,每行一个目标,格式为“类别id 中心点x 中心点y 宽度 高度”,且坐标值必须归一化到 0 到 1 之间。
直接拿官方标注训练必翻车,因为 TT100K 有一百多个类别,里面大量类别只有几十张样本,训练出来的模型会严重偏向高频类。常见做法是先做一次类别合并,把限速类合并成“限速标志”、把禁止类合并成“禁止标志”,把任务从上百类压到 10 到 15 类。类别合并的映射规则要写在一个脚本里,保证训练和推理用的是同一份映射表,否则后面导出模型时会类别错乱。下面这个转换脚本会把 TT100K 的 JSON 标注转成 YOLO 格式的 TXT,同时按面积过滤掉太小的目标,并按映射表合并类别:
import json import os from pathlib import Path # 类别映射表:把 TT100K 的细分类别合并成业务类别 # 格式: 细分类别 -> (业务类别id, 业务类别名) CLASS_MAP = { "i5": (0, "speed_limit"), "i10": (0, "speed_limit"), "i15": (0, "speed_limit"), "i20": (0, "speed_limit"), "i30": (0, "speed_limit"), "i40": (0, "speed_limit"), "i50": (0, "speed_limit"), "i60": (0, "speed_limit"), "i70": (0, "speed_limit"), "i80": (0, "speed_limit"), "i90": (0, "speed_limit"), "i100": (0, "speed_limit"), "p10": (1, "no_stopping"), "p11": (1, "no_stopping"), "p26": (2, "no_entry"), "p19": (3, "no_left_turn"), # 其余类别按需求继续补充 } def convert_tt100k(json_path, img_width, img_height, output_dir, min_area=1024): """把 TT100K 的单个 JSON 标注转换为 YOLO 格式的 TXT 文件""" with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) txt_lines = [] for obj in data.get("objects", []): category = obj.get("category") if category not in CLASS_MAP: continue bbox = obj.get("bbox", {}) xmin, ymin, xmax, ymax = bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"] w = xmax - xmin h = ymax - ymin if w * h < min_area: continue # 过滤掉过小或难以辨认的目标 class_id = CLASS_MAP[category][0] # 计算归一化后的中心点坐标和宽高 x_center = (xmin + w / 2) / img_width y_center = (ymin + h / 2) / img_height norm_w = w / img_width norm_h = h / img_height txt_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}") txt_out = os.path.join(output_dir, Path(json_path).stem + ".txt") with open(txt_out, "w", encoding="utf-8") as f: f.write("\n".join(txt_lines)) if __name__ == "__main__": # 遍历 JSON 目录,逐张转换;img_width 和 img_height 从图片实际尺寸读取 json_dir = "tt100k/annotations" out_dir = "tt100k/yolo_labels" os.makedirs(out_dir, exist_ok=True) for json_file in os.listdir(json_dir): if json_file.endswith(".json"): convert_tt100k( os.path.join(json_dir, json_file), img_width=2048, img_height=2048, output_dir=out_dir )这个脚本的关键逻辑有三处:第一是 CLASS_MAP,它承担了类别合并的核心工作,转换后模型只面对 5 到 15 个业务类别,类别不均衡问题会缓和很多;第二是 min_area 过滤,TT100K 里有大量面积只有几百像素的极小目标,人工标注时本身就存在模糊,强行让模型学这些样本只会增加噪声;第三是归一化坐标,YOLOv5 的 loss 计算全部在归一化坐标系里完成,如果直接填像素坐标,训练时会梯度爆炸。需要特别注意的是,img_width 和 img_height 不能写死,TT100K 原图多是 2048×2048,但经过裁剪后的子图尺寸各异,转换前要先读取实际图片尺寸。
3. 用 YOLOv5 训练自己的数据集:目录、配置与训练参数全拆解
3.1 数据集目录怎么摆:images 和 labels 严格配对
YOLOv5 对目录结构有硬性要求,按它的约定来能让后面少踩很多坑。训练脚本只认 images 和 labels 这两个目录,而且必须保证同名图片和同名 TXT 文件分别位于对应目录下的同一级路径。常见的做法是把数据集组织成下面的结构:
datasets/ └── traffic_sign/ ├── images/ │ ├── train/ │ │ ├── 00001.jpg │ │ └── ... │ └── val/ │ ├── 00100.jpg │ └── ... └── labels/ ├── train/ │ ├── 00001.txt │ └── ... └── val/ ├── 00100.txt └── ...图片和标签文件名必须完全相同,只差扩展名。如果你用上面的转换脚本生成了 TXT,但图片来自另一个目录,需要写个小脚本把同名文件同步过去。我见过不少人在这一步翻车:图片在一个子目录、标签在另一个子目录,训练时 YOLOv5 为了兼容会把缺失标签的图片悄悄跳过,结果训练集的图片数量少了一大截,你还没察觉。验证这种问题的方式很简单,训练日志里会打印每个划分的图片数量和标签数量,数量不匹配就说明路径有问题。
labels 目录下每个 TXT 文件的内容就是上一章转换脚本生成的数据。如果是从零标注自己的数据,用 LabelImg 或 X-AnyLabeling 导出的 YOLO 格式可以直接用,不需要再转换。需要记住的是,TXT 里的类别 id 必须从 0 开始,并且和 data.yaml 里的 names 列表顺序一一对应。这个对应关系是整个训练链路上最容易出乱子的环节,最好把类别映射表单独存一份文件,不要只存在脑子里。
3.2 data.yaml 怎么写得让 YOLOv5 不翻车
data.yaml 是 YOLOv5 的数据接口,训练脚本启动时第一个读它。一个典型的 traffic_sign.yaml 长这样:
# 数据集根路径,可以是绝对路径,也可以是相对 YOLOv5 运行目录的路径 path: /home/user/datasets/traffic_sign train: images/train val: images/val # 类别数量,必须和 names 列表长度一致 nc: 5 # 类别名称列表,索引对应标注文件里的 class_id names: 0: speed_limit 1: no_stopping 2: no_entry 3: no_left_turn 4: no_right_turn这个文件里最容易出问题的三个参数是 path、nc 和 names。path 的坑在于相对路径的解析规则:它相对于你运行 train.py 时的工作目录,而不是相对于 yaml 文件所在位置。我在实际项目里一般直接写死绝对路径,省得换目录后路径失效。nc 写错会导致类别数量不匹配,训练时 loss 正常但检测结果全乱;names 顺序和 TXT 文件里的 class_id 不对应,则会让“限速牌”被显示成“禁止停车”之类完全无关的名字,这个错误在训练阶段还看不出来,只有推理阶段才会暴露,非常阴险。
data.yaml 里还有几个可选参数,例如针对验证集的测试集路径 test。毕设场景下如果不做模型调优比赛,可以不写 test,只用 train 和 val 就够。另外要注意 val 集合不能和 train 集合有任何重叠,尤其是用视频抽帧做数据集时,同一个视频相邻帧视觉上几乎一样,如果被分到两个集合里,验证集的 mAP 会虚高,答辩时换到真实场景就原形毕露。
3.3 启动训练:从预训练权重到 yolov5 超参数的调参顺序
数据准备好了,训练命令就是整套系统的主干。YOLOv5 官方源码仓库里,train.py 支持的参数很多,做交通标识识别项目时最常用的启动命令如下:
python train.py \ --data traffic_sign.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0 \ --cache逐项拆开看:--weights 指定预训练权重,用见到最多的 yolov5s.pt 即可,它是 COCO 数据集的预训练模型,backbone 已经学到了基础的颜色和纹理特征,即使交通标识和 COCO 的类别差异很大,迁移学习仍然比随机初始化收敛快得多;--img 是训练输入尺寸,640 是默认值,如果你在上一章判断出自己的数据集小目标占比高,可以改成 960,但显存不够时优先保住 batch;--batch 决定显存占用和梯度稳定性,16G 显存用 16 比较稳,8G 显存降到 8;--epochs 第一次跑建议 100,先看收敛趋势,后续再补 50 轮;--cache 表示把图片缓存到内存里,能显著减少磁盘 I/O 带来的训练间隙,但占内存,32G 内存以上才建议开。
除了这些命令行参数,还有一份 yolov5 超参数文件 hyp.scratch.yaml 值得手动调。它里面有 lr0、mosaic、mixup、scale 这些数据增强和优化器参数,对交通标识识别影响最大的是 mosaic 和 scale。scale 是随机缩放增强,对自然场景目标很有效,但交通标识是强几何形状目标,尤其限速牌上的数字,过强的缩放会让字符变得模糊,学出来的模型对字符细节不敏感;我把 scale 从 0.5 降到 0.3 之后,限速类别的精确率明显回升。mosaic 在目标小、样本少的场景下推荐保留默认值,它能有效缓解小目标样本不足的问题。
训练过程中,如果发现 loss 持续不降,先检查是不是环境问题而不是模型问题。确认 python 依赖装全了、GPU 驱动正常后,可以加一个 --resume 参数接着上次的权重继续训练,YOLOv5 会把优化器状态同步恢复,不需要重新从头跑。这里有个血泪经验:tensorboard 或 wandb 的日志会输出每个轮次的 mAP,不要只盯着 loss,loss 降到 0.02 但 mAP 只有 0.5 也是可能的,因为交通标识类别不均衡时,模型会把高频类学得很好,低频类干脆放弃,这时候要看每类的 AP 而不只是整体指标。
4. 训练完怎么看账:从损失曲线到模型导出的一整套验证清单
4.1 损失曲线怎么判断模型有没有学歪
训练跑起来后,终端里每一轮都会打印 box_loss、cls_loss、dfl_loss、mAP@0.5、mAP@0.5:0.95 等一堆指标。新手最容易犯的错是看到 loss 在降就觉得万事大吉,实际上 loss 数值本身没有绝对值意义,关键要看验证集上的 mAP 曲线是否同步抬升。如果训练 loss 一路下降、val mAP 却停滞不动,这是典型的过拟合信号——模型把训练集里的背景和噪声一起背下来了,到新场景上就失效。
还有一种情况是 loss 降得很慢、曲线像一条平缓的斜坡,这大概率是学习率没配上。YOLOv5 使用的余弦退火调度器会把学习率从初始值 lr0 逐渐降到接近 0,如果 lr0 设置偏低,模型从头到尾都跳不出局部极小值。排查方法是看训练日志里的当前学习率,如果已经降到 1e-5 量级但 mAP 还在 0.4 以下,说明这一步训练基本到顶了,与其加轮数不如重新调高 lr0 再跑。反而是那些 loss 曲线出现明显震荡的,不用太慌,目标检测训练里前几个 epoch 的 loss 震荡很正常,特别是 mosaic 增强改变了每批数据的分布,模型需要时间稳定下来。
4.2 混淆矩阵和 results.png:确认模型到底认错哪些标志
YOLOv5 训练结束后会在 runs/train/expN 目录下生成 results.png 和 confusion_matrix.png 这两张关键图。results.png 是一组子图,包含 box_loss、cls_loss、precision、recall、mAP@0.5、mAP@0.5:0.95 的全程曲线,基本能覆盖你要在答辩里展示的所有训练过程证据。confusion_matrix.png 则直接回答“模型把哪两类搞混了”这个问题。
以限速标志为例,如果你保留了“限速50”和“限速60”两个独立类别,混淆矩阵里这两类之间的色块大概率会很深。原因不是模型不行,而是这两个类在字符上只差一个数字,在 640 输入尺寸下数字区域可能只有 20 像素宽,人眼都费劲,模型认错太正常了。处理方式有两种:一种是把所有限速子类合成一个大类,只输出“限速”和“解除限速”,把数字识别留给后续的 OCR 模块或留给驾驶员自己看;另一种是增加大量带数字细节的裁剪样本,让模型强行学到字符差异。对毕设或工程落地来说,前者把路线走小了,但把指标做上去了,答辩时更稳;后者看起来更“智能”,但需要付出十倍的数据量,且精度未必尽如人意。我一般选前者,在系统设计上说明“本系统负责检测标志的有无与类别定位,具体数值由附加模块识别”,这个架构本身也是可扩展的。
4.3 导出与推理验证:onnx 导出、固定输入尺寸与后处理参数
训练完成后得到的是
本文还有配套的精品资源,点击获取