简介:这是基于YOLOv5的交通标志物检测项目完整包,面向计算机相关专业正在完成课程设计或期末大作业的学生,以及需要人工智能、图像处理方向实战练习的深度学习开发者。项目整合了可直接运行的检测源码、训练好的模型权重与全部数据,覆盖从数据准备、模型训练到推理检测的完整流程,能够帮助使用者快速搭建交通标志识别系统。资源共266个文件,压缩包约423MB,主要包含yaml配置文件、py训练与推理脚本、jpg/png图片数据集、pt模型文件、sh辅助脚本以及训练日志、Dockerfile等,目录结构清晰,便于按模块查阅和二次开发。项目源自个人97分的期末大作业,代码经过严格调试,下载后即可运行,适合作为课程设计或毕业设计的完整参考,也可用于学习YOLOv5的工程组织、数据组织与训练细节,并在此基础上拓展改进。目前已有362人学习使用。
1. 交通标志检测为什么值得用 YOLOv5 源码包起步
交通标志物检测是目标检测里最典型的“小目标密集+类别多+实时性要求高”场景,YOLOv5 恰好是这些年做课程设计、竞赛和工程原型时最常见的选择。拿到一份带 YOLOv5 源码、训练好的模型权重和完整数据集的工程压缩包,意味着你不用从零开始标数据、不用花两周调环境,核心工作就变成三件事:把数据格式捋顺、把训练参数跑通、把模型部署到实际图片或视频流里。这篇文章就按“源码包里有什么 → 数据怎么对齐 → 训练怎么调 → 模型怎么验证部署”的顺序,把整个落地链路拆开讲,顺带把最容易翻车的几个坑列出来。适合正在做毕设、准备竞赛,或者刚接触 yolov5 目标检测、想用现成资源跑通一个真实项目的开发者。
2. 先拆解 YOLOv5 交通标志检测方案:结构、数据和选型理由
2.1 交通标志检测为什么选 YOLOv5 而不是其他模型
交通标志检测的难点不在“能不能检出”,而在“小目标能不能稳定检出”。路牌在画面里往往只占几十个像素,加上逆光、遮挡、模糊,很多检测模型在通用数据集上表现不错,一换到真实街景就掉点。YOLOv5 在这一点上有几个结构上的优势值得先理解。
它仍然沿用 CSPDarknet 作为骨干网络。CSP 结构把特征图分成两部分,一部分走密集卷积块,一部分直接跨越连接,这样既减少了计算量,又保留了梯度信息。在列车上,骨干网络输出三种尺度的特征图,分别对应大、中、小目标。小目标检测靠的是浅层特征图,分辨率高但语义信息弱,所以 YOLOv5 在检测头前面加了一个 PANet 路径聚合结构,把深层语义信息往浅层特征图上“喂”。这就解释了为什么它在路牌这类小目标场景下比早期 YOLOv3 稳定——不是玄学,是特征融合的路径确实更合理。
另一个实际选型理由是工程成熟度。YOLOv5 的源码组织方式是所有版本里最清晰的,data、models、utils 三层目录职责分明,训练、验证、导出、推理脚本各自独立。对新手来说,改数据路径、换模型配置文件、跑通一次训练,都是直接改参数就能完成的事,不需要碰底层 CUDA 代码。对老手来说,它的分布式训练、混合精度、模型导出 ONNX 的链路也都是现成的,改造成本低。
还有一个容易被忽略的点:YOLOv5 对显存的容忍度比很多新模型好。低显存运行模型是很多学生党的刚需,4G 显存的笔记本 GPU 或者纯 CPU 机器,只要把 batch-size 降到 8 以内、开启梯度累积,照样能训练小规模数据集。这一点在后面的参数设置里会具体展开。
2.2 一份交通标志检测数据集应该包含什么
拿到“全部数据”之后,第一件事不是直接开训,而是先核实数据的组织方式是否匹配 YOLOv5 的预期格式。常见的数据集包会包含 images 和 labels 两个顶层目录,分别存图片和标注文本。标注文本每行对应一个目标,格式是:
class_id x_center y_center width height
注意这里是归一化后的中心点坐标和宽高,不是像素值,也不是 VOC 的 XML 格式。很多现成数据包是从 TT100K、CCTSDB 这类公开数据集转换过来的,但转换过程常常留下一些边界问题,比如类别编号不连续、某些图片没有对应标签文件、像素坐标和归一化坐标混用。这些问题在训练时不会立刻报错,而是表现为 loss 降不下去或者某些类别完全检测不到。
交通标志数据本身还有一个特点需要关注:类别分布严重不均衡。限速标志、禁止标志在数据里可能占了大半,而一些罕见的警告标志只有几十张。YOLOv5 对每个类别都会分配一个检测头,但如果某个类别训练样本过少,这个类别在训练过程中会被其他类别“带偏”。所以拿到数据后要做一个统计,至少要看每个类别的图片数和目标数分布。用一段简单脚本就能做到:
python import os from collections import Counter
label_dir = "labels/train" cls_counter = Counter() total_boxes = 0
for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname), "r") as f: for line in f: parts = line.strip().split() if len(parts) >= 5: cls_counter[int(parts[0])] += 1 total_boxes += 1
print("总目标数:", total_boxes) for cls_id, cnt in sorted(cls_counter.items()): print(f"类别 {cls_id}: {cnt} 个目标")
这段脚本遍历训练标签目录,统计每个类别出现的次数。注意最后一行的循环里我只取了前五列,因为有些标注文件会追加额外的属性列,比如遮挡程度或难度等级,直接取前五列解析成 class_id + 四坐标即可。如果发现某些类别只有个位数目标,建议先做数据增强或者干脆合并粗粒度类别,否则训练出来的权重在那几个类别上基本是碰运气。
2.3 源码包里的模型配置:s、m、l 怎么选
YOLOv5 的 models 目录下有一组以 yaml 结尾的配置文件,常见的是 yolov5s.yaml、yolov5m.yaml、yolov5l.yaml、yolov5x.yaml。它们的差别不是算法结构,而是网络的宽度和深度系数。s 的 width_multiple 是 0.50,depth_multiple 是 0.33;m 是 0.75 和 0.67;l 是 1.0 和 1.0;x 是 1.25 和 1.33。
在交通标志检测这种场景下,我的建议是优先从 s 或 m 起步。原因很直接:路牌目标小但不复杂,类别之间的区分度高,不需要特别深的网络来提取细粒度纹理特征。s 模型在 1080Ti 上做推理能做到 100 FPS 以上,训练一轮也要不了太久。m 模型精度会高一些,主要高在重复检测的抑制上,但代价是训练时间约为 s 的 1.5 倍。如果你的数据集只有几千张图,直接上 l 或者 x 很容易过拟合,验证集 mAP 反而往下掉。
配置文件里还有几个关键数值建议改动。最值得调的是 anchors。YOLOv5 默认的 anchors 是针对 COCO 数据集聚类出来的,COCO 里目标普遍偏大。交通标志普遍是扁平的、小尺寸目标,用默认锚框会导致早期训练不稳定。源码里有自动计算锚框的逻辑,训练时加一行参数即可:
bash python train.py --data data/traffic_sign.yaml --cfg models/yolov5s.yaml --weights '' --batch-size 16 --epochs 100 --img 640 --auto-anchor
auto-anchor 会在训练开始前根据标注数据重新聚类 anchors,并更新到模型配置里。首次训练时这个环节会多花一两分钟,但对小目标检测的收益是立竿见影的。如果你用的是别人训练好的权重继续微调,同样建议先让 auto-anchor 跑一遍,因为原始权重的锚框来自源数据集,不一定适配你的目标尺寸分布。
3. 把源码包跑起来:数据格式对齐、训练命令与关键参数
3.1 数据目录组织与 YAML 配置的坑
YOLOv5 在训练时通过一个 YAML 文件来定位数据集,而不是直接在命令行里写死路径。拿到项目压缩包后,先看有没有这个配置文件,通常叫 data/traffic_sign.yaml 或类似的名字。标准内容应该是这样:
yaml train: ./datasets/traffic_sign/images/train val: ./datasets/traffic_sign/images/val
nc: 6 names: ['warning', 'prohibitory', 'mandatory', 'stop', 'yield', 'guide']
train 和 val 指向的是图片目录,YOLOv5 不直接读标签目录,而是根据图片目录自动推导同级 labels 目录。也就是说 images/train 下的图片 train_001.jpg 会去找 labels/train/train_001.txt,一一对应。推导规则是找图片路径中名为 images 的目录,替换成 labels。如果压缩包里目录结构不规范,比如 labels 和 images 不在同一根目录下,训练时会报出一堆找不到标签的警告,但进程不会中断,只是那部分图片被跳过。
很多项目包里的 YAML 文件名带空格或中文,这本身不是问题,问题是 Windows 环境下某些版本的 YOLOv5 在加载时对编码敏感。保险做法是把数据集目录和配置文件路径都保持纯英文,中间不要有空格。路径的另一种常见翻车是用了相对路径且从别的目录启动训练脚本,导致数据集找不到。建议在 train.py 所在目录下运行命令,或者把 YAML 里的路径写成绝对路径。
nc 和 names 必须和数据集的标注一致。这里的第一个坑是 nc 写错。比如数据里有 6 个类别,但 YAML 里 nc 写成 5,训练不会报错,只是最后一个类别的所有标注会被当成 out-of-range 数据丢掉。真正坑的是 names 和标注里的 class_id 对不上,因为训练时类别索引是从 0 开始的,names 只是打印日志用的,不会用来校验标注。这意味着如果你发现训练日志里显示类别名和实际内容对不上,说明数据标注本身可能有问题,需要重新核对标签文件。
3.2 用训练好的权重跑通第一张图:最小推理命令
模型训练前,先验证包里的预训练权重能不能正常加载和推理。这一步能帮你把环境问题、路径问题和模型结构问题一次性暴露出来。假设你有权重文件 best.pt,放在 runs/train/exp/weights 下,先跑一张测试图:
bash python detect.py --weights runs/train/exp/weights/best.pt --source test_images/street_01.jpg --img 640 --conf 0.25 --iou 0.45
detect.py 执行后会在 runs/detect/exp 下生成标注了检测框的结果图。上图如果能看到标志被框出来且类别正确,说明源码和权重都是可用的。如果结果图全空,先调低 conf 阈值到 0.1 试一试,很多时候不是模型没检测到,而是置信度比你设的阈值低。真实道路场景中,远处的小路牌只有 0.2 左右的置信度,这在很大程度上是正常的。
推理时还有一个关键参数是 imgsz,也就是送入网络前图片被缩放到多大。默认是 640。交通标志检测建议推理时设成 960 或 1280,代价是推理速度下降,但小目标召回率会明显提升。原因是标志在 640 尺寸下可能只有 20 个像素宽,经过三次下采样后特征图上就剩几个点了,特征信息所剩无几。把输入撑到 960 后,同样一个标志在特征图上能占据更多像素,检测头更容易命中。
3.3 从头训练的命令模板与 6 个必调参数
预训练权重能跑通之后,如果你要训练自己的数据集,YOLOv5 支持两种初始权重策略。第一种是用 COCO 预训练权重做迁移学习,适合数据量小但场景分布接近的情况,这是最常见的做法。第二种是完全空白权重,也就是 weights 参数设为空字符串,模型会从随机初始化开始训练,通常不推荐,因为收敛慢且很容易在交通标志这种细节分辨任务上陷入局部最优。
一个适合交通标志数据集从头训练的模板命令如下:
bash python train.py
--data data/traffic_sign.yaml
--cfg models/yolov5s.yaml
--weights yolov5s.pt
--batch-size 16
--img 640
--epochs 150
--optimizer SGD
--lr0 0.01
--cos-lr
--workers 4
--device 0
这个命令里有六个参数是在调参时会反复碰到的,逐一说明。batch-size 受显存限制,16 在 8G 显存下基本是上限;如果只有 4G 显存,降到 8,并加上 --accumulate 4 用梯度累积保住等效批次大小。img 是训练输入尺寸,交通标志建议 640 起步,显存充裕可以提到 800。epochs 不要少于 100,这类数据集小,训练充分与否看的是模型对背景干扰的抑制能力,轮次少了容易欠拟合。optimizer 推荐 SGD 而不是 Adam,因为 Adam 在早期收敛快,但最终 mAP 通常不如调好学习率的 SGD。lr0 初始学习率 0.01 是 YOLOv5 的默认推荐值,数据集越小越要保守一点,数据只有几百张图建议降到 0.005。cos-lr 启用余弦退火学习率,它会让学习率在训练后期平滑下降,对稳定最终权重有明显帮助。
参数不是设完就完事。训练过程中要盯住两个东西做动态调整:边界框损失和类别损失。YOLOv5 每个 epoch 都会把损失汇总打印到终端。正常情况下前 20 轮总损失会从两位数快速降到个位数,然后进入缓慢下降区间。如果损失在前 10 轮不降反升,基本可以确定是学习率过大,停掉把 lr0 减半再继续。如果损失在 80 轮后还在明显波动,说明模型在验证集上可能过拟合了,这时候改成加载之前 checkpoint 继续训不如重新跑一个更短 epoch 的训练。
4. 交通标志物检测落地中的 5 个高频坑:现象、原因、处置
4.1 显存不足导致训练中断
现象:训练跑到第几个 batch 时直接报 CUDA out of memory,有时候还会连带把整个 Python 进程搞崩,桌面花屏。
原因:imgsz 过大或 batch-size 超出显存容量。很多人拿默认的 batch-size 16、imgsz 640 直接跑,在 4G 显存上必炸。YOLOv5 训练时缓存图片的机制也会额外占用显存,特别是开启了 cache-images 参数后,整个数据集直接映射进显存。
解决:先看一眼 GPU 显存总量,nvidia-smi 命令即可。4G 到 6G 显存,batch-size 设为 8,imgsz 保持 640,不要开 cache。6G 到 8G,batch-size 16 可以跑,但尽量关掉其它占显存的应用。如果 batch-size 降下来后梯度噪声变大导致训练不稳定,加 --accumulate 4,等效于用 32 的 batch-size 更新一次梯度。
提示:训练时不要开 TensorBoard 以外的浏览器页面,Chrome 对显存的占用有时候大到足以让训练中断。
4.2 标签文件报错 class id 超出范围
现象:训练日志里频繁出现 WARNING 提示 label class 超出 nc 范围,同时对应图片被跳过。严重时训练完看 mAP,某些类别结果为零。
原因:数据集整理时类别编号没有重新从 0 开始连续编号,或者 YAML 中 nc 统计错了。比如原始数据有 8 类,你删掉 2 类后标注文件里还残留 class_id 6、7,但 nc 设成了 6,于是训练时所有编号大于等于 6 的标注全部无效。
解决:写一个小脚本扫描所有标签文件里的类别编号分布,前面 2.2 节那个脚本就可以用。确认最大编号后,把 nc 设为最大编号加一。如果某些类别确实不要了,用脚本把对应标注行删掉,不要只改 YAML,否则训练时标签文件残留的旧编号会持续产生告警。
python import os
label_root = "labels" keep_classes = {0, 1, 2, 3, 4, 5}
for sub, _, files in os.walk(label_root): for fname in files: if not fname.endswith(".txt"): continue path = os.path.join(sub, fname) with open(path, "r") as f: lines = f.readlines() new_lines = [] for line in lines: parts = line.strip().split() if not parts: continue if int(parts[0]) in keep_classes: new_lines.append(line) with open(path, "w") as f: f.writelines(new_lines)
这个脚本会遍历所有标签文件,把不在 keep_classes 集合里的目标整行删掉。注意脚本里读写都在同一个路径,执行前建议把原始数据备份一份。别觉得这个步骤多余,很多项目包里的所谓“全部数据”,其实是别人从多个数据集合并后直接灌进 YOLOv5 的,类别编号混乱是常态。
4.3 训练损失下降但验证 mAP 很低
现象:训练 loss 正常下降,到 100 轮后趋近稳定,但 val 模式跑出来的 mAP 只有 0.2 左右,比同样数据量的其它项目差一大截。
原因:这种情况八成是训练集和验证集数据分布不一致。压包里如果训练集和验证集来自不同数据源,比如训练集全是中国路牌,验证集里混了欧洲路牌,模型在验证集上自然会翻车。另一个常见原因是默认的 10% 划分逻辑,如果数据目录本身没有划分 val,YOLOv5 会自动从训练集抽 10% 当验证集,这会导致有标注错误的数据同时出现在训练和验证里,评估结果虚高。
解决:自己动手重划分数据。用 sklearn 的 train_test_split 或者手写一个按 9:1 划分的脚本,确保划分前把同一个场景的连拍帧归到一起。交通标志数据集中常常有同一块路牌在视频连续帧里的多张截图,如果不做场景聚类直接随机划分,这组极度相似的图片会同时出现在训练和验证中,mAP 虚高到 0.8 以上,实际部署时立刻原形毕露。
python import os import shutil from sklearn.model_selection import train_test_split
image_dir = "datasets/traffic_sign/images_full" label_dir = "datasets/traffic_sign/labels_full" images = [f for f in os.listdir(image_dir) if f.endswith(".jpg")]
train_imgs, val_imgs = train_test_split(images, test_size=0.15, random_state=42)
for split, imgs in [("train", train_imgs), ("val", val_imgs)]: os.makedirs(f"datasets/traffic_sign/images/{split}", exist_ok=True) os.makedirs(f"datasets/traffic_sign/labels/{split}", exist_ok=True) for img_name in imgs: base = os.path.splitext(img_name)[0] shutil.copy( os.path.join(image_dir, img_name), os.path.join(f"datasets/traffic_sign/images/{split}", img_name), ) if os.path.exists(os.path.join(label_dir, base + ".txt")): shutil.copy( os.path.join(label_dir, base + ".txt"), os.path.join(f"datasets/traffic_sign/labels/{split}", base + ".txt"), )
random_state 固定为 42 是刻意为之,目的是一次划分后不确定的部分可以复刻。实际使用时建议换几个随机种子跑一两轮,观察 mAP 波动幅度。波动大说明数据集规模不够或者标注质量不稳定,这时候优先补数据,比调模型更有效。
4.4 小目标漏检严重:远距离路牌完全没反应
现象:近处大路牌能正确识别,但图片中远景处的限速牌、人行横道牌全部漏检,甚至一些中等距离的指示牌也时有时无。
原因:除了前面提到的输入尺寸问题,还有一个是 NMS 阈值设置。交通标志密集出现在同一区域时,多个检测框重叠,默认的 iou 0.45 会把部分正确检测框当作重复框抑制掉。另外,如果训练时 imgsz 是 640,但推理时直接提到 1280,输入分辨率变化会改变小目标的响应强度,但分布变了模型不一定适应得过来。
解决:先做推理侧调整。把 imgsz 从 640 提到 960,conf 阈值降到 0.2,看召回率有没有提升。如果提升明显,说明问题出在输入尺度上,需要重新在 imgsz 960 下微调模型。训练侧的做法是开启多尺度训练,在 train.py 命令里加上 --multi-scale,模型每个 batch 会从 480 到 960 之间随机取尺寸,让模型对目标尺度变化更鲁棒。注意 multi-scale 训练会显著增加训练时间,显存占用也会上升。
提示:小目标漏检的排查顺序永远是先看输入尺寸,再看 anchors,最后才动网络结构。大部分场景下前两招已经能解决 80% 的问题。
4.5 换机器后权重加载失败或推理结果异常
现象:在 A 机器上训练好的权重,拷贝到 B 机器后,用同样的源码加载,报错 Missing keys 或者 Unexpected keys。就算加载成功,推理结果也和原来完全不一致。
原因:一种情况是源码版本不一致。YOLOv5 不同版本之间的模型定义细节有差别,v5.0 训练出来的权重直接塞给 v7.0 的模型,结构对不上是必然的。另一种情况是权重文件路径被別的东西覆盖了,压缩包里 root 目录和子目录下各有一个同名 pt 文件,加载时选错了一个。
解决:加载前先确认两边的源码版本一致,最简单的方法是比对 models/yolo.py 里的 class Detect 定义。然后是核对权重文件的哈希值,用 sha256 校验拷贝前后文件是否一致。最后,如果加载时出现 missing keys,不要硬加载,先看一下当前模型配置文件的 yaml 和权重训练时使用的 yaml 是否同一个文件,最常见的就是 model 的名字相同但 anchor 数量不一致。
python import torch
ckpt = torch.load("best.pt", map_location="cpu") print(ckpt.keys()) print(ckpt["model"].yaml if "model" in ckpt else "no model key")
这段脚本读取权重文件后打印模型配置文件信息。如果这里显示的 yaml 结构物和你当前源码里的不一致,基本可以断定是跨版本加载。跨版本加载不是不能解决,但需要自己写权重迁移脚本,非必要不建议走这条路,直接找对应版本的源码重新训练或者推理更省时间。
5. 验证模型效果并部署:从 mAP 指标到实际视频流推理
5.1 用 val.py 评估训练好的模型:看哪几个指标
训练不结束就不知道模型行不行,YOLOv5 的验证脚本在训练过程中会自动跑,但为了拿到一个可靠的最终结果,训练完成后要单独用 val.py 手动跑一次全量验证。命令如下:
bash python val.py
--weights runs/train/exp/weights/best.pt
--data data/traffic_sign.yaml
--img 960
--conf 0.001
--iou 0.6
--half
注意这里我把 conf 设成了 0.001,这不是为了让结果好看,而是为了计算 mAP 时把所有置信度档位覆盖全。mAP 的计算逻辑是统计不同置信度阈值下 precision 和 recall 的曲线面积,如果验证时 conf 阈值太高,曲线起点就会缺失,mAP 值会失真。iou 0.6 是 COCO 风格的标准设置,表示预测框和真实框重叠度超过 60% 才算正确命中。
验证结果会输出一个表格,里面逐类列出 precision、recall、mAP@0.5 和 mAP@0.5:0.95。对交通标志检测来说,mAP@0.5 是最关键的参考值,它表示预测框大致位置正确即可判定,无需像素级精确。因为路牌本身形状规整、边缘清晰,mAP@0.5 达到 0.85 以上基本满足部署要求。mAP@0.5:0.95 是更严苛的指标,它要求框的重叠度在 0.5 到 0.95 之间逐级计算,路牌如果存在标注框偏移,这个值会很难看,所以这个指标的绝对值不必过度纠结,只需要对比训练前和训练后是否提升。
还有一个需要人工复核的维度是类别混淆。val.py 会生成一张混淆矩阵图,保存在 runs/val/exp/confusion_matrix.png。查看这张图,重点看对角线值是否远大于非对角线值。如果出现某两类互相误判严重,比如“禁止驶入”和“禁止通行”混在一起,说明这两类的视觉特征过于接近,要么数据标注本身有争议,要么需要增加这两类在训练集中的样本量。
5.2 把模型部署到视频流:用 detect.py 跑摄像头实时检测
模型验证通过后,部署的最常见形态是视频流检测。YOLOv5 的 detect.py 本身就支持从摄像头或 RTSP 流读取,命令比图片推理多一个参数:
bash python detect.py
--weights runs/train/exp/weights/best.pt
--source rtsp://192.168.1.100:554/stream1
--img 960
--conf 0.25
--iou 0.45
--max-det 20
--max-det 20 是建议加入的参数,它限制了单张图片最多输出 20 个检测框。交通标志场景里一个画面往往同时存在多个标志,但绝不会超过二三十个,限制数量可以防止误检的零散小框刷屏,也为后处理节省时间。如果摄像头是 USB 摄像头,source 改成 0 即可,0 代表第一个设备编号。
视频流部署时最容易忽略的是帧率匹配。detect.py 默认逐帧处理,如果你的 GPU 推理速度是 50ms 一帧,摄像头输入是 30 FPS,那么处理速度跟不上输入,画面会出现堆积延迟。解决思路有两个:一是把输入帧率降到 15 FPS,在摄像头端或者用 OpenCV 的 VideoCapture 设置 CAP_PROP_FPS;二是改用跳帧策略,只处理关键帧。YOLOv5 的 detect.py 没有内置跳帧逻辑,但你可以把视频流先经过一段采样脚本,再送入推理,适合用在边缘设备上。
5.3 ONNX 导出与运行时部署:脱离 PyTorch 环境的移植
如果部署目标是嵌入式设备或纯 C++ 环境,PyTorch 权重不能直接使用,需要先导出为 ONNX 格式。YOLOv5 自带 export.py,一条命令完成转换:
bash python export.py
--weights runs/train/exp/weights/best.pt
--img 960
--batch-size 1
--include onnx
--simplify
--simplify 会调用 onnx-simplifier 对计算图做常量折叠和冗余节点消除,这个操作通常让模型体积减小 10% 左右,推理速度提升约 20%。导出的 onnx 文件可以用 ONNX Runtime 在 CPU 上跑,也可以用 TensorRT 转成 engine 文件在 N 卡上跑。
导出过程中有一个常见坑:PyTorch 模型里的 opset 版本和运行时库支持的版本不一致。如果运行时报 unsupported operator,优先检查两边的 opset 版本。export.py 可以通过 --opset 参数指定,一般设 12 或 13 兼容性最好。另一个坑是动态尺寸,默认导出的是固定尺寸 960x960 的静态图,如果推理时输入的图片尺寸不是整数倍,模型会报尺寸不匹配。交通标志场景建议固定尺寸导出,部署时统一做 letterbox 填充到 960x960,比使用动态维度在多数设备上更稳定。
导出后验证一步不可少:同一张图分别用 PyTorch 权重和 ONNX 模型跑推理,比对检测框坐标和类别。YOLOv5 的导出脚本会顺带做一次校准,输出一个参考结果,但如果那一帧恰好没有目标,校准就形同虚设。建议自己挑一张包含多个不同类别标志的实拍图,对比两边的输出框坐标误差,误差超过 2 个像素就需要检查预处理逻辑是否一致,特别是归一化系数和颜色通道顺序。
6. 把交通标志检测做成可靠方案的三个进阶技巧
第一,学会用小目标切片推理绕过特征丢失问题。在做检测车道路牌这种长宽比极端的画面时,全图直接缩放会让标志在输入图上变得过小。我的做法是把原图按横向切成三段,每段独立推理再将结果拼回原坐标。这个技巧不改变模型,但对召回率的提升比换大模型更明显,而且切图后的推理天然支持多线程并发,三个线程各跑各的分段,最终延迟几乎不变。代价是相邻切片边界上的目标可能被切开,所以我一般让每段之间重叠 30 像素,重叠区域内的检测框取置信度更高的那个。
第二,在类别不平衡数据集上做针对性增强。交通标志数据集中,警告标志和禁令标志数量通常碾压指路标志。老手不会在模型结构上折腾,而是在数据增强上动手。YOLOv5 在训练时默认开启 mosaic 增强,但它的含义是四张图拼在一起,如果每张图里都是同一类标志,增强效果反而加剧了类别不平衡。一个简单的替代方案:把少数类图片在每次 epoch 前重复复制两次到训练目录,相当于在采样阶段做了类别重加权。这个做法比改损失函数省事得多,效果也更直接。有个前提是复制图片时要对应复制标签文件,并且用随机重命名避免和原图重叠。
第三,做一次跨场景泛化测试。模型在验证集上分数高,不代表实际部署没问题。我习惯在训练结束后,手机到附近路口拍十张不同光线、不同角度的路牌照片,直接喂给 detect.py 跑一遍,要求每张图至少能检测出 80% 的标志。这步测试能暴露出数据增强的盲区,比如你的训练数据全是白天晴天,模型可能在逆光场景下漏检。发现这个问题后,用 OpenCV 的 HSL 空间随机调整亮度做一轮增强再微调,比重新标注数据快得多。
这套流程走下来,你会意识到交通标志检测这个项目真正的难度不在模型,而在数据编排和部署细节。我踩过最深的一次坑是花了三天调模型结构,最后发现数据目录里有一批图片的标签是错的,类别名称和编号错位,所有努力白费。从那以后我的习惯是训练前永远先跑一遍标签检查脚本,推理前永远先用一张实际拍而不是网上下载的图片做冒烟测试。希望这个习惯也能帮你少走弯路,项目落地顺利。
本文还有配套的精品资源,点击获取