简介:面向毕业设计、课程设计与期末大作业场景,这套基于YOLOv5的道路交通标识识别系统提供完整可运行的Python源码与配套数据集。项目经过实际调试,配备图形化界面与基本管理功能,代码含详细注释,适合具备一定深度学习基础的学生快速上手、按需修改,并可灵活扩展识别类别与界面布局。资源包共266个文件,压缩后约423MB,其中53个py文件覆盖训练、检测与界面逻辑,59个yaml文件包含模型配置及数据集参数,10个pt文件为预训练权重,55张jpg与26张png构成图片样本集,另含sh脚本、Dockerfile与ipynb说明,便于环境搭建和流程复现。当前已有50人浏览学习。下载后可直接部署运行,既能支撑高评分课题答辩,也有助于理解目标检测从数据准备、模型训练到推理展示的完整链路,对完成毕设或课设具有实际参考价值。
1. 道路交通标识识别,难的不是YOLOv5而是数据本身
做道路交通标识识别毕设,最常见的一个动作就是把YOLOv5 clone下来、拿个公开数据集开训,然后卡在同一个地方:loss降下去了,val mAP也有零点几,但把模型放到一段真实行车视频里,限速牌漏检、停止牌误检、小目标基本全丢。这不是YOLOv5不行,而是交通标识识别这个场景对数据和标注的要求,比“通用目标检测demo”苛刻得多。
道路交通标识识别系统的完整落地路径,其实落在三件事上:数据集怎么选、标注怎么转成YOLO格式、超参数怎么针对小目标调。YOLOv5只是中间那个执行者。本文按一条完整可复现的路线走一遍——从选数据集开始,到训练出能用的权重,再到导出做真实场景验证,把路径上的坑逐个填平。适合正在做毕业设计、课程项目,或者想快速验证“检测方案能不能用于交通标识”的从业者。
2. 为什么是YOLOv5而不是YOLOv8/SSD:交通标识识别场景的选型逻辑
2.1 YOLOv5的网络结构里,哪些模块在为你干活
YOLOv5的Backbone是CSPDarknet,Neck是PANet,Head是三个不同尺度的输出。这三个尺度对应了交通标识识别里最关键的一件事:小目标。道路交通标识在画面里占比通常只有几个百分点,甚至远距离时只有几十个像素。YOLOv5在P3层(80×80特征图)上负责小目标检测,这也是为什么用YOLOv5做这个场景,比用SSD或Faster R-CNN更顺手——它的多尺度架构天生覆盖了“近处大牌、远处小牌”这种动态范围。
PANet的作用是从高层语义往低层细节做路径增强。直观来说,深层特征图知道“这是个交通标识”,浅层特征图知道“它的边缘在哪”,PANet把这两者融合起来,小目标的定位精度才有保障。如果你改用YOLOv5s之前的旧版或者盲目砍掉某个层,远距离标识的框会明显偏移。
实际选型时,我一般建议直接上YOLOv5s起步。在交通标识识别这种类内差异小、类别数量有限的场景里,YOLOv5s的推理速度和精度平衡最好,GTX 1660 Super级别就能以40ms左右跑一帧。不必一开始就上YOLOv5x,那是等模型在中等尺寸输入下已经收敛但精度不够时,再考虑用更大模型来榨取剩余精度的路数。
2.2 交通标识识别与通用检测的三个本质差异
交通标识识别虽然用的是通用检测器,但它有三个不能忽略的特殊性,训练策略要围绕这三条来设计。
第一是类别极度不平衡。限速标志、警告标志在真实路面上出现频率远高于“禁止驶入”这类牌,而公开数据集TT100K里的类别分布就是长尾的。直接拿原始数据集训练,模型会对头部类别过拟合,尾部类别几乎不响应。处理方式要么做类别重采样,要么用YOLOv5自带的类别损失加权。这一点在3.1节具体展开。
第二是颜色和语义强相关。交通标识的边框颜色、底色本身就携带了分类信息,比如红色圆框表示禁令,黄色三角表示警告。这对YOLOv5来说其实是有利条件,因为CSPDarknet提特征时会天然学到颜色分布。但反过来也有坑——夜间或逆光场景下颜色信息退化,模型就很容易把“解除限速”这类灰底标识漏掉,甚至把红色刹车灯误检成禁令标志。所以训练数据里一定要混入夜间和逆光样本,否则Feature层面的颜色先验会变成误检来源。
第三是目标尺度跨度极大。同一张图里,近处的限速牌可能有300像素宽,远处的可能只有15像素。YOLOv5的anchor机制在默认COCO参数下是按物体比例分布的,对交通标识这种“小且接近正方形”的目标并不适配。因此训练前需要用YOLOv5自带的k-means脚本重新聚类anchor,这是训练前最容易被跳过的关键步骤。
3. TT100K与CCTSDB转YOLO格式:标注脚本与边界框修正
3.1 数据集选型:TT100K、CCTSDB、自制数据怎么搭配
道路交通标识识别绕不开的数据集有三个:TT100K、CCTSDB和德国GTSRB。GTSRB是分类数据集,不能直接用,排除。TT100K是腾讯开源的,在真实街景上截取,含10万张图片、156个类别,是最接近真实路况的选择,但它的标注数量并不均衡,大量类别只有个位数样本。CCTSDB是长沙理工的交通标志数据集,标注了“指示”“警告”“禁令”三个大类加具体类别,图像多来自行车记录仪,场景比TT100K更脏、更多样,更适合用来做泛化验证。
我一般建议的搭配是:主训练集用TT100K,把其中样本数最多的45个类别筛出来作为训练范围,再混入CCTSDB里对应类别的数据做领域扩充。如果做毕设,这个量已经足够支撑一个像样的检测器。不要贪多求全把156个类别全训,很多类别样本数不足10张,硬训只会让loss崩掉。
如果你有自制数据,比如自己拍了学校周边的路牌,用LabelImg标注时要注意:YOLO格式要求的是归一化后的中心点坐标加宽高,而LabelImg默认导出的是Pascal VOC格式的左上右下坐标,两者需要转换。LabelImg导出时不要勾选“PascalVOC”,直接选YOLO格式即可,否则后面还得写脚本转一次。
3.2 转YOLO格式的完整脚本
TT100K的官方标注是JSON格式,给出的坐标是包围盒左上角坐标加宽高。以下脚本把TT100K的标注转成YOLO训练所需的txt格式,并按类别筛选保留常见标识。
import json import os from PIL import Image # 类别筛选:只保留样本数足够多的45类 KEEP_CLASSES = ["i5", "i4", "i2", "p6", "p3", "p5", "w55", "w57", "w59", "i8", "p11"] # 实际使用时请根据你的数据情况补全为45类,这里仅作示例 def convert_tx2k(json_path, img_dir, out_dir, keep_classes): os.makedirs(out_dir, exist_ok=True) with open(json_path, 'r', encoding='utf-8') as f: annos = json.load(f) for img_name, anno in annos['imgs'].items(): img_path = os.path.join(img_dir, img_name) if not os.path.exists(img_path): continue # 读取图片宽高,标注归一化需要用到 img = Image.open(img_path) img_w, img_h = img.size # 原标注是物体字典形式,按objects字段逐项读取 objects = anno.get('objects', []) txt_lines = [] for obj in objects: cls = obj['category'] if cls not in keep_classes: continue # TT100K标注是 [x_min, y_min, x_max, y_max],且可能是旋转框 xmin, ymin, xmax, ymax = obj['bbox'] x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h # 过滤掉异常框:归一化后的坐标不在0-1内说明标注越界 if not (0 <= x_center <= 1 and 0 <= y_center <= 1): continue if box_w <= 0 or box_h <= 0: continue class_idx = keep_classes.index(cls) txt_lines.append(f"{class_idx} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") if txt_lines: txt_name = os.path.splitext(img_name)[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(txt_lines)) # 同时复制图片到同一目录,方便YOLO训练时直接引用 img.save(os.path.join(out_dir, img_name)) convert_tx2k( json_path='annotations.json', img_dir='images', out_dir='yolo_labels', keep_classes=KEEP_CLASSES )这段脚本有几个关键点需要说明。TT100K的标注坐标存在越界现象,训练时YOLOv5会对越界的GT自动裁剪到图像范围内,但如果是“宽或高为0”的无效标注会直接报错退出。脚本里的坐标合法性过滤就是为这个准备的,属于训练前一劳永逸的排雷手段。
另一个细节是TT100K的标注里bbox字段在某些子集里可能是一个旋转框的四点坐标,而不是标准数组。我在实际转换时遇到过标注Field名字不一致的情况,如果你的json结构对不上,先用以下命令快速检查某一帧标注里有哪些字段,再针对性调整解析逻辑,不要盲目套脚本:
import json with open('annotations.json') as f: annos = json.load(f) # 取一张带标注的图,打印一下标注结构,确认字段名再转换 first_key = list(annos['imgs'].keys())[0] print(annos['imgs'][first_key]['objects'][0].keys())3.3 边界框归一化的坑
YOLO格式要求坐标归一化到0~1,TT100K的标注在1920×1080的原图上算正常,但有些开源子集做过resize,标注框没有跟着缩放。如果在训练时发现loss一直不降或者mAP在0.3以下徘徊,先在脚本里加一行打印,确认转换后的框宽高是否都在合理范围:
import random # 随机抽取10行txt,打印归一化坐标人工检查 sample_file = random.choice(os.listdir('yolo_labels')) with open(os.path.join('yolo_labels', sample_file)) as f: for line in f.readlines(): print(line.strip())正常情况下的归一化坐标,x_center和y_center都应该在0.1~0.9之间,box_w和box_h至少应该大于0.001。如果你看到box_w超过0.8甚至达到1.0,那说明原始标注宽高方向搞反了,或者box存储的是多边形而不是矩形,需要回看原图上对应位置重新标注。这个检查步骤最多10分钟,但能省掉下游数小时的排错时间。
4. 用YOLOv5训练交通标识模型:环境配置与超参数怎么设
4.1 conda环境与依赖安装
YOLOv5对Python版本不算挑剔,3.8到3.10都能跑。建议用conda建独立环境,避免和系统Python打架。以下是一套经过验证的安装过程,适用于CUDA 11.8和PyTorch 2.0的组合:
# 创建独立环境,Python版本固定3.9 conda create -n yolov5 python=3.9 -y conda activate yolov5 # CUDA 11.8对应的PyTorch安装命令 pip install torch==2.0.0 torchvision==0.15.0 --index-url https://download.pytorch.org/whl/cu118 # 克隆YOLOv5仓库并安装依赖 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt依赖安装完成后,先跑一次检测验证环境是否正常,这一步能排查PyTorch和CUDA是否真正打通:
python detect.py --weights yolov5s.pt --source data/images/bus.jpg如果你用的是纯CPU机器,把torch安装命令换成CPU版本即可,但训练会是煎熬——一个epoch在几百张图上可能要跑几十分钟。毕设场景如果完全依赖CPU,建议直接把训练集压缩到2000张以内,或者租个云端GPU来跑,自己电脑只做推理验证。
4.2 数据配置YAML与训练启动
YOLOv5训练前需要准备一个data yaml文件,描述数据集路径和类别名称。以下是交通标识场景的标准配置:
# traffic_sign.yaml train: data/traffic_sign/images/train val: data/traffic_sign/images/val nc: 45 names: 0: 'i5' # 限速5 1: 'i4' # 限速40 2: 'i2' # 限速20 3: 'p6' # 禁止停车 4: 'p3' # 禁止通行 # ... 按你的类别表顺序补齐45个注意:这个yaml里的train和val路径是图片目录,YOLOv5会自动到同级目录下找labels文件夹读取txt标注。因此你的目录结构必须严格是images/train/xxx.jpg配合labels/train/xxx.txt,不能把图片和txt混放在一起,否则训练时标注文件全部丢,模型会退化成无监督,loss直接崩。
启动训练的命令我一般这么写:
python train.py \ --data traffic_sign.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 150 \ --cache ram \ --no-autoanchor--no-autoanchor是我特有的一步。前面提过,默认anchor是为COCO的大目标设计的,交通标识偏小,需要单独聚类得到自己的anchor。先用下面的命令输出适合你数据集的anchor:
python utils/autoanchor.py --data traffic_sign.yaml --img 640 --max-det 100命令会把训练集里的GT框做k-means聚类,重新生成9组anchor。把这9组数值填到模型的yaml文件里,然后再正常训练。跳过这一步,小目标的召回率会明显偏低,mAP看起来尚可但实际检测时远处的牌基本漏完。
4.3 超参数:图像尺寸、batch、epoch、学习率
图像尺寸是最核心的超参数。交通标识属于小目标,理论上是输入分辨率越高对小目标越友好。但我实测过,从640加到960,小目标mAP提升大约5个点,训练时间却翻了一倍多。如果你的显卡显存只有8G,老老实实用640,不要硬上960,否则batch size被迫降低,梯度噪声变大,精度反而下降。12G以上显存可以考虑896。
batch size选择,建议8~16之间。显存不够时有两条路:一是降低batch,二是开启--cache ram把图片预加载到内存里加速喂数据,后者能显著减少IO瓶颈。epoch设在150左右比较合理,一般到80~100轮就会看到val mAP增速趋缓,此时加载最后一次val mAP最高的权重做后续验证就行。
学习率参数里,YOLOv5默认的lr0=0.01对COCO分类数(80类)有效,而交通标识通常只有几十类。训练时如果看到loss在前10个epoch不降或者直接发散,把--lr0降到0.005重试,同时在训练命令里加一句:
python train.py ... --lr0 0.005 --lrf 0.01lrf是最终学习率相对初始学习率的比例,设置0.01能让训练后期学习率衰减得更低,有助于fine-tune边界框回归精度。在训练结束后,记得查看runs/train/exp/weights/best.pt,之后的所有验证都以这个文件为准,不要用last.pt,它保存的是最后一个epoch的权重,通常不是最优状态。
5. YOLOv5交通标识避坑:小目标漏检、类别混淆与误检排查
5.1 现象:夜间/雨雾天气漏检骤增,调高conf阈值也无济于事
原因:训练集里白天顺光样本占绝大多数,模型学到的本质上是“光照充足环境下的标识纹理”。夜间标识牌反光、颜色失真,特征分布整体偏移,置信度天然低。调高conf阈值只会过滤更多低置信度框,漏检反而更严重。
解决:在训练集里按15%~25%的比例混入夜间和昏暗环境样本。如果没有现成数据,用OpenCV对白天样本做数据增强也能缓解——降低亮度到原图的0.6倍、加高斯噪声,模拟夜间视觉退化。
import cv2 import numpy as np img = cv2.imread('day_sample.jpg') # 模拟夜间低照度 dark = cv2.convertScaleAbs(img, alpha=0.6, beta=0) # 加轻度噪声,模拟传感器劣化 noise = np.random.normal(0, 8, img.shape).astype(np.uint8) night_like = cv2.add(dark, noise) cv2.imwrite('night_like.jpg', night_like)将增强后的图片和原图一起放入训练集,重新训练后夜间漏检率能明显下降,这一步是我做交通标识项目性价比最高的操作,没有之一。
5.2 现象:限速40和限速80互相误检,告警乱报
原因:交通标识的类别间差异极其细微。40和80的圆环、红边、白底结构完全一致,只有数字不同。在低分辨率下,40和80的纹理差异只有几个像素,模型在这两个类别上的特征空间重叠严重。
解决:第一优先级是为“数字敏感”类别补充更多近景高清样本,让模型有机会学到数字纹理。如果样本补充困难,改用--cls损失权重,把这两个类别的损失系数调高,强制模型加大区分力度:
python train.py \ --data traffic_sign.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 150 \ --cls 0.7 # 默认0.5,调高以强化分类约束第二种做法是在后处理阶段做约束:对同一目标连续帧检测时,如果出现“40→80→40”的跳变,则认为是误检,取置信度更高的类别作为结果。这个逻辑放在应用层而不是模型层,改动成本低,效果立竿见影。
5.3 现象:mAP有0.8但实测视频里框在抖动
原因:mAP是“静态图片加固定阈值”的评估指标,它不考虑预测框在时间序列上的稳定性。视频连续帧里,模型对同一块标识的置信度轻微波动,导致目标框在“存在”和“不存在”之间振荡,表现为框跳变。
解决:在推理端叠加一个轻量级策略,连续三帧中至少两帧检出同一位置目标才输出告警,否则保持上一状态。以下是一个简单实现:
class SignTracker: def __init__(self, threshold=2): self.history = {} self.threshold = threshold def update(self, detections): confirmed = [] for det in detections: cls = det['class'] box = tuple(det['box']) key = (cls, box) self.history[key] = self.history.get(key, 0) + 1 # 连续出现的次数达到阈值才确认为真目标 if self.history[key] >= self.threshold: confirmed.append(det) return confirmed这段代码的核心思想是给检测结果加一个“信任积分”,避免单帧噪声引起告警。实际部署时用队列保存最近5帧的历史状态,配合records历史窗口做滑窗统计即可。当前frame的检测框与上一帧的匹配用IoU判断,IoU超过0.5视为同一目标,这种方式能平滑掉80%以上的抖动。
5.4 现象:训练loss正常但val mAP一直不涨
原因:train和val的数据分布差异过大。常见的情况是从TT100K和CCTSDB各取了一半数据,但两个数据集的标注格式、图像分辨率不同,train里TT100K占比高,val里CCTSDB占比高,模型自然在val上表现差。这时loss虽然是下降的,但val mAP不涨就是分布偏移的报警信号。
解决:先检查数据拆分。YOLOv5训练前会自动按比例划分数据集,但如果你手动指定了train和val路径,务必确认两边类别分布一致。一个简单做法是:
# 统计train和val的类别数量分布 find data/traffic_sign/labels/train -name '*.txt' | xargs cat | awk '{print $1}' | sort | uniq -c find data/traffic_sign/labels/val -name '*.txt' | xargs cat | awk '{print $1}' | sort | uniq -c对比两列数字,如果某一类别在train里出现500次,在val里只出现5次,那这个类别的mAP天然方差极大。解决方式是采用分层抽样,按类别分布将数据切分为train/val,保证每个类别的比例一致,再开始训练。
6. 从PT权重到线上验证:ONNX导出与置信度阈值调优
训练完的best.pt只是起点。实际做毕设展示或部署时,建议导出成ONNX格式,这样能在CPU上获得两倍以上的推理速度提升,也能脱离PyTorch环境运行。
import torch from models.experimental import attempt_load # 加载训练好的权重 model = attempt_load('runs/train/exp/weights/best.pt', map_location='cpu') # 导出ONNX,opset版本11以上兼容性较好 dummy_input = torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, 'traffic_sign.onnx', opset_version=11, input_names=['images'], output_names=['output'], dynamic_axes={'images': {0: 'batch_size'}} ) print("ONNX导出完成")导出时需要注意opset版本。opset 11与OpenCV DNN模块和ONNXRuntime的兼容性最佳;opset 12以上虽然支持更多算子,但部分旧设备上运行时可能出现算子不支持的问题。如果导出后推理报错,优先降低opset重试。
置信度阈值是部署时最需要细调的一个参数。训练阶段,YOLOv5默认用conf=0.25作为验证阈值,但实际道路场景里,漏检的代价通常比误检更高(漏掉一个限速牌可能直接产生交通违法)。我建议部署时把conf降到0.15~0.2,用NMS的IoU阈值0.45来控制框重叠,而不是靠提高conf来过滤。调低conf之后误检框会变多,但配合5.3节的连续帧确认逻辑,最终告警的精准度仍然可控。
一个值得做的验证实验是:把训练好的模型放到一段5分钟的行车视频上跑一遍,统计“检出目标数”和“漏检目标数”。这个数比mAP更能说明真实场景下的可靠性。测试时用以下命令输出每一帧的检测结果并保存到文件,方便逐帧排查:
python detect.py \ --weights runs/train/exp/weights/best.pt \ --source road_test.mp4 \ --conf 0.2 \ --iou 0.45 \ --save-txt \ --save-conf--save-txt会把每帧检测到的类别、坐标和置信度写入txt文件,--save-conf额外保存置信度分数。之后拿到这些数据,和视频逐帧人工比对,就能准确找出是哪一类、什么距离下发生了漏检。
最后想分享一个做这个项目的个人习惯:几乎所有交通标识识别的翻车现场,最后排查下来都不是网络结构的问题,而是数据配比、标注质量或者阈值设错。所以我的做法是训练完不急着调模型结构,先打印一组bad case——就是置信度很高但明显标错的框、置信度低但确实存在的漏检框,看完这组图再决定下一步动数据还是动模型。这个习惯帮我省下了大量试错时间,希望能帮到你。
本文还有配套的精品资源,点击获取