☰
YOLOV5交通标志识别实战:数据集转换到夜间场景调优全流程
2026/10/8 1:04:01 网站建设 项目流程

简介:基于YOLOv5的交通标志识别检测项目,面向毕业设计、课程设计与期末大作业等场景,提供从数据集标注、模型训练到推理部署的完整闭环。资源共266个文件、压缩包约423MB,核心包括53个Python源码、10个PT预训练权重、YAML模型配置、图像样本与TXT标签文件,同时附带Dockerfile、Shell脚本和TensorBoard事件文件,整体目录组织清晰,可直接复现训练流程或进行二次开发。项目代码注释细致,关键模块均经过严格调试,普通环境下简单配置即可运行,新手也能快速上手;训练结果CSV与事件日志可辅助理解模型收敛过程,便于论文与答辩材料中的结果分析。该方案可整体作为毕业设计或期末大作业使用,曾获导师高度认可,目前已有105人浏览学习,适合需要高效完成高质量视觉检测项目的学生参考借鉴。

1. 交通标志识别项目为什么绕不开 YOLOV5、数据集与模型这三件套

毕业设计拿到“交通标志识别检测”这个题目时,大多数人第一反应是:这不就是拿 YOLOV5 训练一个模型嘛。真做起来你会很快发现,模型反而是最不用操心的部分,真正卡住人的是交通标志数据集的整理、标注格式的转换、以及训练完成之后怎么把模型效果讲清楚。这个题目能拿高分,通常不是因为你把 YOLOV5 的代码改得多花哨,而是因为你把数据、训练、评估这条链路走得完整且可复现。这篇笔记会从交通标志数据集的选型与清洗,到 YOLOV5 的自定义训练、推理导出,再到常见训练踩坑和夜间场景调优,完整讲一遍我实际跑这个课题时的做法,新手能照着复现,熟手也能对照参数和边界。

2. 数据集的筹备与转换:拿到交通标志图之后第一件事是清洗标注

2.1 交通标志检测数据集怎么选:公开数据集与自采数据的分工

交通标志识别在学术界有个常见误区:一上来就找 GTSRB。GTSRB 是分类数据集,一张图一个标志,只有类别标签,没有目标框。做检测你需要的是带标注框的数据集,所以更常见的选择是 TT100K——腾讯街景全景下的交通标志数据集。TT100K 覆盖了多种标志类别,拍的是真实街景,车型、遮挡、光线变化和国内交通标志的样式都贴近实际场景。另一个可用来源是 Mapillary Vistas,但它更偏向分割,坐标要二次转换,预处理成本高。

我给毕设项目的建议是双轨走:先用公开数据集把模型基线跑通,再采一批校园或城市道路的实景图做泛化验证。公开数据集负责证明你的模型结构和训练流程正确,自采数据负责证明你的方案在真实场景下可用,这正好对应毕设评审里“模型有泛化能力”这一条。自采数据不需要太多,300 到 500 张包含标志的图片就够,但每张图都要检查标注框是不是把标志完整框住。实际经验是,公开数据集里的标志通常又大又正,自采图里经常出现路边杆子上的侧视角标志,这类图不参与训练,单独作为验证集会给你一个很扎心的结果:训练 mAP 很高,实拍图检测一塌糊涂。

TT100K 的原生标注是 JSON 格式,类别定义也不完全是简单的数字 ID。直接拿它喂 YOLOV5 之前,我一般会把 JSON 统一转成更通用的 VOC XML 中间格式,再用脚本转成 YOLO 需要的 txt。这样做的原因是后续如果换数据集,比如换成 CCTSDB 或自己标注的数据,中间格式的转换脚本可以复用。CCTSDB 是长沙理工发布的交通标志检测数据集,标注格式就是 VOC 风格,很多做这个题目的同学最后都落在 CCTSDB 和 TT100K 的混合训练上。混用前一定要检查两类数据集的类别 ID 是否对齐,常见翻车点是 TT100K 里禁止鸣喇叭和限速标志在 CCTSDB 里被分到不同类,不合并直接训练会导致模型对类别边界非常混乱。

2.2 把 VOC 格式转成 YOLOV5 需要的 txt:转换脚本与四个边界坑

YOLOV5 的标注格式是每个 txt 文件一行,内容为 class_id x_center y_center width height,坐标全部是归一化后的小数,中心点和宽高都除以图片宽高。VOC 格式则是左上角和右下角坐标,且可能是像素值。所以转换逻辑并不复杂,但有不少边界情况要处理。下面这段脚本是我在项目里用的精简版,覆盖了单类别和多类别两种场景,并且处理了坐标越界和图片读取失败。

import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_dir, img_dir, out_dir, class_names): os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() img_name = root.find('filename').text img_path = os.path.join(img_dir, img_name) if not os.path.exists(img_path): print(f'[WARN] image not found: {img_path}') continue try: w = int(root.find('size/width').text) h = int(root.find('size/height').text) except AttributeError: w, h = Image.open(img_path).size out_lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) xmin = float(obj.find('bndbox/xmin').text) ymin = float(obj.find('bndbox/ymin').text) xmax = float(obj.find('bndbox/xmax').text) ymax = float(obj.find('bndbox/ymax').text) xmin, xmax = max(xmin, 0), min(xmax, w) ymin, ymax = max(ymin, 0), min(ymax, h) if xmax <= xmin or ymax <= ymin: print(f'[WARN] invalid box in {xml_file}: {name}') continue x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h out_lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}') txt_name = os.path.splitext(xml_file)[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w', encoding='utf-8') as f: f.write('\n'.join(out_lines))

这段脚本里有个容易被忽略的细节:我优先从 size 节点读宽高,读不到时再回退到 PIL 读图。VOC 标注里的 size 和图片真实尺寸不一致的情况在公开数据集中不少,如果不做这个回退,转换出来的归一化坐标全是错的,模型训练时 loss 会异常低,但推理出来的框全部偏移。第二个坑是 xmin、xmax 没有裁剪,部分标注框会超出图片边界,归一化后 width 大于 1,YOLOV5 训练时会报 “assertion” 或者边界框损失发散。第三个坑是类别名不在 class_names 里时直接忽略,而不是报错,这样能避免单个脏标注拖垮整个训练流程。第四个坑是 txt 里如果没有标注对象,YOLOV5 允许空文件训练,但很多版本会跳过该图片,导致实际训练样本变少。

参数说明:class_names 是类别列表,顺序就是最终模型的类别 ID 顺序。这个列表要一直保持到后续 data.yaml 里,不能中途调整顺序。xml_dir 和 img_dir 可以是同一目录,也可以分开,我这里分开传是为了兼容 CCTSDB 这类图片和标注分目录存放的数据集。

2.3 划分 train/val 并生成 data.yaml:毕设里最容易丢分的环节

数据和标注整理好后,接下来要做的不是立刻训练,而是先把目录结构和配置文件安排好。YOLOV5 对 datasets 目录的默认约定是 images 和 labels 分开,而 labels 里没有 jpg 文件。我习惯用下面的目录结构:

datasets/ tsign/ images/ train/ img_001.jpg val/ img_002.jpg labels/ train/ img_001.txt val/ img_002.txt

这个结构的好处是后续做增量训练时,图片和标注不会混在一起,也不容易让 YOLOV5 的缓存机制误判文件类型。划分数据集时,不要用 random.shuffle 之后按百分比硬切。交通标志数据集里往往有同一时间段连续拍摄的帧,硬切会把同一场景的相似图片同时分进 train 和 val,验证集指标虚高。我一般先按图片前缀分组,再按组划分,比如连续帧的视频帧全部落在同一边。

接着是 data.yaml,这是 YOLOV5 自定义训练的核心配置,内容不多但每个字段都直接影响训练结果。

path: C:/workspace/datasets/tsign train: images/train val: images/val nc: 3 names: 0: speed_limit 1: warning 2: stop

path 字段在 Windows 下建议写绝对路径,且使用斜杠而不是反斜杠。很多同学在这里写相对路径,结果训练时因为当前工作目录不同而导致找不到图片,报错信息又不太直白,最终浪费一整天排查。train 和 val 是相对于 path 的目录,注意这里只写 images 下的子目录,YOLOV5 会自动去同级 labels 目录找标注。nc 是类别总数,names 下面每个类别的名称尽量用英文小写,避免中文类别名在控制台或 tensorboard 里出现编码乱码。

还有一个细节是训练前删除缓存文件。YOLOV5 第一次读取数据时会生成.npy 缓存,如果后续你重新生成了标注 txt,旧的缓存会把更新后的标注过滤掉,导致模型怎么训练都看不到新数据。我每次整理完数据集后都会执行一次清理命令:find datasets/tsign -name '.npy' -delete。这个动作看似多余,但实际是交通标志这种多轮迭代项目的后悔药。

3. 从零跑通 YOLOV5 训练交通标志:环境、权重与超参

3.1 Python 环境与 YOLOV5 源码准备:版本锁定比什么都重要

先交代一个背景:YOLOV5 是近几年目标检测领域应用最广的开源项目之一,网络结构、训练策略和后处理代码都是完整可用的开源实现。做毕设时不需要自己重写模型,直接把源码下载到本地,按 requirements 安装依赖。需要注意 Python 版本,常见的合适范围是 3.8 到 3.10,太新的 Python 版本会碰上 torch 不兼容的问题。

git clone https://github.com/ultralytics/yolov5 cd yolov5 python -m venv venv venv/Scripts/activate pip install -r requirements.txt

虚拟环境这步建议不要省。交通标志训练过程中要频繁试不同的数据集和超参数,依赖冲突是常见翻车点。如果你之前装过其他深度学习框架,直接用全局环境很容易出现 torch 版本不对或者 numpy 版本冲突。requirements.txt 里没有固定所有包的上限,所以装完依赖后顺手看一眼 torch 和 CUDA 的匹配关系,命令是 python -c "import torch; print(torch.version, torch.cuda.is_available())"。CPU 机器也能训练,但交通标志的小目标比较多,CPU 训练一个 100 轮的模型可能要十几个小时,有条件还是用 GPU。

Git 克隆的是源码,但实际训练时会自动下载预训练权重 yolov5s.pt,国内网络下可能很慢。常见做法是手动下载后放到 yolov5 根目录的 weights 文件夹。预训练权重的作用是提供 COCO 数据集上学到的特征提取能力,训练交通标志时有了它,模型收敛速度和最终精度都会明显更好。如果你完全从随机权重开始训练,mAP 通常低 5 到 8 个点,而且训练轮数要多一倍。

3.2 训练命令与关键参数:img、batch、hyp 的取舍

数据和环境就绪后,训练命令如下。这里我以 yolov5s 模型为例,因为它速度精度均衡,适合绝大多数毕设环境。

python train.py \ --data data/tsign.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0 \ --project runs/tsign \ --name exp1

这个命令里几个参数值得细说。--img 640 是输入图片分辨率,交通标志本身是小目标,在 640 分辨率下很多标志只有十几个像素。如果显存够用,我建议把 img 改成 960 甚至 1280,模型对小标志的召回率会显著提升,缺点是训练时间和显存占用都会上升。batch 16 是显存不够时的折中值,如果你的卡是 8G 显存,batch 16 加 img 640 是安全的;如果是 4G 显存,就降到 8。epochs 100 是常见基线,我实际跑下来交通标志数据量在 5000 张左右时,70 轮以后 val mAP 就基本不再涨了,100 轮只是为了留出足够的收敛窗口。

隐藏参数里最值得动的是 hyp。YOLOV5 默认的超参数文件是 data/hyps/hyp.scratch-low.yaml,里面包含学习率、数据增强强度、损失权重等几十项。在没有明显欠拟合或过拟合的情况下,默认值就是最优解,不要为了显得自己“调过参”去乱改。真正要动的是 fl_gamma,这是 focal loss 的 gamma 参数,专门应对正负样本不平衡。交通标志数据集中负样本背景占比极大,我把 fl_gamma 从默认 0.0 调整到 1.5,能让模型更关注那些难分类的小标志,代价是训练速度稍慢。

结合热门的“yolov5 超参数”玩法,我还会在训练时加一条命令看超参数变化曲线,方便判断模型是否已经收敛:

tensorboard --logdir runs/tsign

TensorBoard 里重点看三个曲线:Box Loss、Objectness Loss 和 mAP_0.5。如果训练到后段 Box Loss 还在明显下降,说明数据集容量还可以容纳更高容量模型;如果 mAP_0.5 在验证集上出现平台期甚至下滑,说明过拟合已经开始了。

3.3 训练结果与模型文件解读:best.pt 和 last.pt 的差别

YOLOV5 训练结束后,runs/tsign/exp1/weights 目录下会生成 best.pt 和 last.pt 两个权重文件。best.pt 是验证集上 mAP 最高的模型,last.pt 是最后一轮的模型。做毕设时使用 best.pt 是默认选择,但有一个例外:如果训练过程不稳定,best.pt 可能是某次验证碰巧跳到高点的模型,而 last.pt 反映的是稳定状态。判断方法是同时跑两遍验证集,或者查看 results.csv 里 best_epoch 前后的 mAP 波动幅度。

训练完成后生成的结果是一整套评估文件,包括混淆矩阵 confusionMatrix.png、PR 曲线、F1 曲线和验证集图片。这个文件夹本身就是毕设展示素材。在答辩里,你不需要展示源码细节,而是用那几张 PR 曲线和混淆矩阵说明“我的模型在各种类别上的误检率是多少”。这也是很多高分毕设的共同特征:不只是会跑训练命令,而是能把评估结果讲成一个完整故事。混淆矩阵里的对角线数值如果低于 0.8,就要回头检查该类别训练样本数量,样本数少于 200 的类别基本别指望高召回率。

4. 训练后模型的使用与交付:从批量推理到导出部署

4.1 用训练好的 best.pt 跑批量推理并落地结果

模型训练完成后,最常见的使用方式是直接调用 detect.py 对测试视频或图片做推理。命令如下:

python detect.py \ --weights runs/tsign/exp1/weights/best.pt \ --source test_video.mp4 \ --conf-thres 0.25 \ --save-txt \ --save-conf \ --project runs/detect \ --name tsign_test

这里 --conf-thres 0.25 是置信度阈值,交通标志的遮挡场景比较常见,阈值设得太高会漏检。--save-txt 会把每个目标的类别、置信度、坐标写入 txt 文件,这也是后续做检测精度评估的基础。--save-conf 让输出的 txt 里带上置信度,方便你在统计结果时按不同阈值重算指标。

如果不想每次推理都用命令行,常见做法是把模型封装到一个 Python 推理脚本里,这样后续做 web 展示或者刚需的 GUI 界面都方便。下面的脚本是精简版,调用的是 torch.hub 的 YOLOV5 自定义模型加载接口,输入一张图,输出检测框和标签。

import torch from PIL import Image model = torch.hub.load('ultralytics/yolov5', 'custom', path='runs/tsign/exp1/weights/best.pt', force_reload=True) model.conf = 0.25 model.iou = 0.45 model.max_det = 50 img = Image.open('test_frames/frame_0012.jpg') results = model(img, size=640) results.print() results.show() boxes = results.xyxy[0].numpy() for b in boxes: x1, y1, x2, y2, conf, cls = b print(f'{model.names[int(cls)]} {conf:.3f} ' f'{int(x1)} {int(y1)} {int(x2)} {int(y2)}')

这段脚本里的 force_reload=True 在网络好的情况下每次都会重新下载依赖权重,如果你已经本地训练好了模型,建议把它改成 False,避免每次重启进程都等半小时。model.conf 和 model.iou 分别是置信度阈值和 NMS 的 IoU 阈值,做交通标志识别时我通常把 IoU 设成 0.45,太低会保留大量重叠框,太高会把两个离得近的话标志合并成一个框。model.max_det 限制单张图最多输出 50 个目标,主要是防止路牌文字被模型误识别时输出一堆无意义的小框。

4.2 导出 ONNX 并跑一遍 C++/移动端的部署思路

毕设里如果提到部署,ONNX 导出几乎是必讲项。YOLOV5 的 export.py 会把 PyTorch 权重转成 ONNX 格式,命令如下:

python export.py \ --weights runs/tsign/exp1/weights/best.pt \ --include onnx \ --opset 12

opset 12 是老少咸宜的版本,既不会被新版运行库拒绝,功能上也够用。导出完成后会生成 best.onnx,你可以直接用 ONNX Runtime 在 Python 里验证一致性,对比 PyTorch 和 ONNX 的推理结果。两者的输出可能有小数点后第四位的误差,这是正常的,因为 ONNX 的 grid 采样和上采样算子在不同实现里会有细微差异,不用零误差对齐。

移动端的部署可以用 ONNX 转 NCNN 或 TensorRT,但在毕设答辩中,你只需要说清楚“ONNX 作为中间表示,可以进一步转换到边缘设备”就够了。真正在项目里跑,更建议先把 ONNX Runtime 的推理搞清楚,毕竟 Android 或嵌入式设备的部署工程量是另一门课,不是这个毕设的核心。

我自己的习惯是导出完 ONNX 后再跑一次同样的测试图片,看两边的目标框数量是否一致。如果数量不一致,排查方向集中在输入图像的预处理上:YOLOV5 训练时用的是 RGB 图,detect.py 里会做 letterbox 填充,推理脚本里也必须做同样的 letterbox。忘掉这一步是 ONNX 推理结果在边角位置目标检测全部失败的头号原因。

5. 交通标志训练避坑指南:5 条能救命的血泪经验

5.1 训练集 mAP 高,验证集 mAP 崩盘

现象是训练 50 轮后 train mAP 已经到 0.96,但 val mAP 还停留在 0.7 附近。原因是数据集划分不随机,很多连续帧图片被同时分进了训练集和验证集,导致模型实际上见过验证集图片。解决方法是按场景分组划分,或者直接使用 YOLOV5 自带的 val 逻辑,保持图片与视频帧来源一致时要先去重。如果数据量足够,更稳妥的做法是按时间段划分,前 70% 时间段的帧做训练,后 30% 做验证。

5.2 训练 loss 正常但检测框全部偏移

现象是 loss 曲线漂亮,mAP 也不低,但可视化结果里框的位置偏左上或右下。原因大多是归一化坐标计算错误。检查手段是随便打开一个 label txt,确认 x_center 是否在 0 到 1 之间,width 是否与图片尺寸比例相符。我见过一个典型错误:把 xmin 和 ymin 当作中心点直接归一化,模型永远学不到正确位置。解决方法是回到 2.2 的转换脚本,对照 VOC 原始标注重新检查三张图的中间结果。

5.3 小目标交通标志漏检率居高不下

现象是限速标志在远处只有 20 像素宽时,模型完全无感知。原因是输入分辨率太低,且模型下采样倍数让特征图上的小目标信息丢失。YOLOV5 是 P3 到 P5 的三层特征图,20 像素的目标在 P3 层只占 10 个像素左右,特征细节很少。解决方法是把 --img 从 640 升到 960 甚至 1280,如果显存不够就减小 batch。另一个可选方案是使用 SAHI 这类切片推理工具,把大图切成小块分别检测,但推理时间会成倍增加。

5.4 Windows 下训练中断,报错 FileNotFoundError

现象是明明图片路径存在,但训练到一半报找不到 labels 缓存文件。原因是 YOLOV5 在 datasets 路径中写入 cache 文件时的路径拼接与 Windows 反斜杠冲突,尤其是 path 字段写了相对路径时更容易触发。解决方法是把 data.yaml 里的 path 写成绝对路径,且全部用正斜杠,并在训练前删掉 labels 目录里所有 *.cache 文件。另外一个相关性非常高的问题是中文用户名,Windows 用户名带中文时,PyTorch 的 DataLoader 偶发崩溃,临时方案是给项目换一个纯英文路径。

5.5 夜间场景训练完,白天效果还行夜间直接失效

现象是验证集 mAP 0.85,晚上拿手机一拍,标志旁边没有杆子的反光就检测不到。原因是训练集以白天为主,模型学到的是亮度和色调分布的浅层特征,而不是标志的形状和颜色特征。解决方法是加入夜间增强策略,最基本的是把训练集里的部分图片做亮度降低、对比度提高、色温偏蓝处理,再复制进训练目录。更为系统化的方式是修改 hyp 文件里的 HSV 增强参数,让模型在训练过程中主动适应低照度。这一步放到下一章细说。

6. 让模型更进一步:夜间场景调优和评估报告的做法

交通标志识别这个题目的评分点里,“夜间场景泛化”是非常容易拉开差距的一处。公开数据集很少包含夜间图片,自采数据的夜间图也更难收集。我一般会在训练时做三步增强:第一步在 hyp 文件里把 hsv_h 从 0.015 调高到 0.02,hsv_s 从 0.7 调高到 0.9;第二步用数据扩充脚本把 20% 的训练图做随机亮度扰动,模拟夜间路灯和车灯的明暗交替;第三步在验证集里专门留出 100 张夜间图片,用来观察模型的掉点幅度。这三步做下来,夜间 mAP 通常能从 0.4 涨到 0.6 左右。

另一个被很多同学忽略的实用技巧是给模型加测试时增强。YOLOV5 的 detect.py 自带 --augment 参数,推理时会做多尺度预测,对远处小标志的漏检有不错改善。代价是单帧推理时间从 0.02 秒涨到 0.1 秒左右,对静态图片评估完全可接受,但如果要做实时视频,这个方案就不太适合。

最后说说评估报告怎么写。答辩时评委最认可的不是“我调参调得多辛苦”,而是一张张可复现的曲线。我会把 runs 目录下的 results.csv 导出成图表,把混淆矩阵和 PR 曲线粘贴到论文实验章节,再单独写一节“失败案例分析”,挑 3 到 5 张模型漏检的图,分析是遮挡问题、小目标问题还是夜间光照问题。坦白说,主动展示失败案例反而能让答辩更有说服力,因为这说明你真的跑过足够多的真实数据,而不是只在公开数据集上刷了点好看的数字。

做这个项目到今天,我最大的教训是永远不要跳过数据集清洗直接开训。YOLOV5 本身太成熟了,网络结构几乎不需要改动,数据才是这个毕业设计的胜负手。希望这篇笔记能帮你少走我走过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询