☰
基于YOLOv5的交通标志识别检测:从数据集处理到模型训练实战
2026/10/8 23:20:36 网站建设 项目流程

简介:面向高校毕业设计场景的YOLOV5交通标志识别检测项目,集数据集、源码与训练模型于一体,适合深度学习初学学生以及需要快速完成毕设、课设、期末大作业的开发者使用。资源包共266个文件,压缩包约423MB,内含yaml配置(网络结构与训练参数)、jpg/jpeg样本图片、py核心代码、pt预训练权重、sh部署脚本及说明文档等,覆盖从数据准备到模型推理的完整链路。代码附有详细注释,关键逻辑均有解释,即便新手也能读懂;项目已通过严格调试,部署简单、界面直观,并附带训练日志与评估记录,便于复盘指标变化和排错调优。目前已有105人浏览学习,可直接作为导师认可的98分毕业设计高分方案,也可在此基础上迁移至其他目标识别场景,具有扎实的扩展价值。

1. Python基于YOLOV5的交通标志识别检测:这道毕设题目到底在考什么

不少选“Python基于YOLOV5的交通标志识别检测”这道题的同学,把GitHub上的YOLOv5仓库clone下来,找到detect.py塞一张带交通标志的图片进去,看到框出来就以为自己完成了毕设。但答辩老师问“你的数据集怎么处理的”“为什么用这个损失函数”“mAP为什么这么低”时,往往答不上来。基于YOLOv5的交通标志识别检测,真正的工作量不在跑通demo,而在三件事:第一,把来源复杂的交标数据集转成YOLOv5能吃的格式;第二,把训练参数调得能匹配小目标、类别不平衡这些交通标志场景;第三,能用指标和可视化定位问题,而不是反复换网络。这个项目适合以毕设、课设或者入职作品为目标的人,下面这条路径能让你从零到一复现出来,也让你在答辩时有东西可讲,少踩一些没必要的坑。

2. 选对数据集:交通标志识别从TT100K到YOLO格式的转换

2.1 主流交通标志数据集怎么选:CCTSDB和TT100K的取舍

常见的交通标志识别数据集有TT100K、CCTSDB、GTSRB和LISA,网上经常搜到的是TT100K和CCTSDB的下载地址,但它们标注格式差别很大。TT100K是腾讯街景全景图上的交通标志,类别超过100个,每张图分辨率较高,小目标多,而且很多类别的样本数量呈长尾分布。CCTSDB是长沙理工发布的国内道路场景数据集,只有prohibitory、warning、mandatory三个大类,标注是VOC格式,类别少所以训练难度低,适合用来走通整个毕设流程。

数据集类别数标注形式是否适合YOLOv5直接训练典型问题
CCTSDB3VOC XML适合类别太少,展示效果不够细
TT100K100+JSON需要筛选转换类别不平衡,小目标多
GTSRB43CSV/单标志图不适合检测没有检测框,需要人工生成
LISA47CSV/框标注可用美国标志,与国内差异大

对于毕设来说,我一般建议先用CCTSDB快速验证环境,再用TT100K里筛选出来的子集做最终模型。如果两者混合使用,需要把VOC和JSON统一成YOLO的txt格式后再合并,否则标签ID会错乱。选择TT100K时先看标注文件里的category字段,只保留样本数超过200的类别。交通标志里的“直行”“限速40”“限速80”这些类别语义清晰,答辩时容易解释;而一些生僻类别比如“注意横风”“禁止拖拉机”样本少,验证集上mAP总是0,反而拉低整体分数。

2.2 把TT100K的JSON标注转成YOLO txt:脚本与四个边界坑

YOLOv5训练时读取的标签是跟图像同名的txt文件,每行格式是class_id x_center y_center width height,四个坐标都除以图像宽高归一化到0到1。TT100K的官方annotations.json里,每个图像的objects数组包含category和bbox,bbox是最小外接矩形的四元组。下面脚本可以完成转换:

import json import os from pathlib import Path json_path = 'annotations.json' img_root = 'train' out_label_root = 'labels_train' img_w, img_h = 2048, 2048 classes = ['i2', 'i4', 'i5', 'i6', 'pl40', 'pl80'] os.makedirs(out_label_root, exist_ok=True) with open(json_path, 'r', encoding='utf-8') as f: anns = json.load(f) count = {c: 0 for c in classes} for img_name, ann in anns['imgs'].items(): img_path = os.path.join(img_root, img_name) if not os.path.exists(img_path): continue objects = ann.get('objects', []) txt_path = os.path.join(out_label_root, Path(img_name).stem + '.txt') lines = [] for obj in objects: cat = obj['category'] if cat not in classes: continue x_min, y_min, x_max, y_max = obj['bbox'] w = x_max - x_min h = y_max - y_min if w < 16 or h < 16: continue x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h w_norm = w / img_w h_norm = h / img_h # 坐标截断:防止归一化后出现负数或大于1 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w_norm = min(w_norm, 1.0) h_norm = min(h_norm, 1.0) lines.append(f"{classes.index(cat)} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}") count[cat] += 1 if lines: with open(txt_path, 'w') as f: f.write('\n'.join(lines)) print('转换结果', count)

这段脚本重点说明四个容易踩坑的地方。第一,TT100K的类别名不是从0开始的连续ID,必须自己维护一个classes列表,classes.index(cat)才是YOLOv5用的类别ID,这个映射一定不能错。第二,img_w和img_h应该读每张图的实际尺寸,而不是统一写死,因为TT100K原始图像经过切分后尺寸可能不同。第三,过滤宽高小于16像素的目标,这类小标志放大后基本是马赛克,留着只会增加训练时的噪声。第四,坐标截断非常必要,街景中标志可能被裁剪到图像边缘,导致bbox超出图像范围,YOLOv5在计算anchor时会得到非法值。

转换完成后,最好随机抽一张图,把txt标签画回原图,直观检查类别ID和框的位置对不对。看不到这一步的人,往往训练到一半发现loss不降,回头检查才发现标签全画歪了。

2.3 训练集验证集划分与目录组织:别让指标虚高

YOLOv5读取数据靠data.yaml里的train和val路径,它要求你的数据放在类似的结构中:

mkdir -p datasets/tt100k/images/{train,val} mkdir -p datasets/tt100k/labels/{train,val}

把图像放进images,对应txt放进labels,两个目录的文件名必须一一对应,只是后缀不同。很多初学者把标签和图像放在一起,然后在data.yaml里只写图像目录,运行时会报“No labels found”,因为YOLOv5会自动把图像路径的images替换成labels去找同名txt。

划分训练集和验证集时,如果只做随机洗牌,同一个标志牌的多张不同角度照片可能同时落在训练集和验证集里,验证mAP会比真实场景高不少,答辩时你用摄像头一测就露馅。虽然没有完美的场景分组信息,一个可用的办法是:先把图像按文件名前缀分组,因为同一地点拍摄的TT100K图片通常有共同前缀;然后把整组分配而不是逐张随机分配。实现时可以用一个简单的哈希分组脚本:

import os import random import shutil random.seed(42) img_files = [f for f in os.listdir('train') if f.endswith('.jpg')] # 按文件名前缀分组,这里假设前缀是下划线或点之前的部分 groups = {} for fname in img_files: key = fname.split('_')[0] groups.setdefault(key, []).append(fname) keys = list(groups.keys()) random.shuffle(keys) split_idx = int(len(keys) * 0.8) train_keys = set(keys[:split_idx]) val_keys = set(keys[split_idx:]) for key in train_keys: for fname in groups[key]: shutil.copy(os.path.join('train', fname), f'datasets/tt100k/images/train/{fname}') label_name = os.path.splitext(fname)[0] + '.txt' if os.path.exists(os.path.join('labels_train', label_name)): shutil.copy(os.path.join('labels_train', label_name), f'datasets/tt100k/labels/train/{label_name}') for key in val_keys: for fname in groups[key]: shutil.copy(os.path.join('train', fname), f'datasets/tt100k/images/val/{fname}') label_name = os.path.splitext(fname)[0] + '.txt' if os.path.exists(os.path.join('labels_train', label_name)): shutil.copy(os.path.join('labels_train', label_name), f'datasets/tt100k/labels/val/{label_name}')

这个脚本每次跑之前要把datasets/tt100k/images和labels目录清空,不然重复复制会留下旧文件。划分完成后,分别统计训练集和验证集每个类别的数量,确认验证集每个类别至少有一张图。如果某个类别训练集有300个样本,验证集只有1个,这个类别的mAP波动会非常大,一次随机种子不同,结果可能从0.6掉到0.1。

2.4 从CCTSDB的VOC格式生成YOLO标签:第二套数据备用

如果你选用CCTSDB,它给的是Pascal VOC风格XML,每个XML里有size和多个object,object里的name是prohibitory、warning、mandatory之一。可以用下面的脚本统一转成YOLO标签:

import xml.etree.ElementTree as ET from pathlib import Path voc_dir = 'Annotations' save_dir = 'labels' Path(save_dir).mkdir(exist_ok=True) class_mapping = {'prohibitory': 0, 'warning': 1, 'mandatory': 2} for xml_file in Path(voc_dir).glob('*.xml'): tree = ET.parse(xml_file) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in class_mapping: continue bndbox = obj.find('bndbox') x_min = float(bndbox.find('xmin').text) y_min = float(bndbox.find('ymin').text) x_max = float(bndbox.find('xmax').text) y_max = float(bndbox.find('ymax').text) x_center = ((x_min + x_max) / 2) / img_w y_center = ((y_min + y_max) / 2) / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h lines.append(f"{class_mapping[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: save_path = save_dir / (xml_file.stem + '.txt') with open(save_path, 'w') as f: f.write('\n'.join(lines)) print('VOC labels converted.')

这个脚本和TT100K脚本本质相同,只是源标注格式不同。CCTSDB的类别只有3个,训练速度快,适合验证整条流程再换TT100K精调。注意CCTSDB有的XML里name可能有大小写或空格,比如“Prohibitory”,脚本里的strip()能处理尾部空格,但大小写不一致会直接跳过该目标。转换后统计一下生成文件数量,如果远少于图像数量,说明类别名映射错了,先把XML里的name全部打印出来看一遍。

3. 在本地训练自己的YOLOv5模型:数据配置与超参数调整

3.1 Python环境与依赖安装:版本对齐是第一道坎

YOLOv5官方仓库本身就是一个可下载的代码包,里面包含模型定义、训练脚本和工具函数。开始之前先在机器上安装Python,推荐使用3.8到3.10的版本,因为PyTorch对Python 3.11以上的支持在某些CUDA版本上不够顺畅,容易出现“找不到torch”的怪问题。然后创建虚拟环境,不要直接装在系统Python里,否则后续装依赖会把环境弄乱。

python -m venv yolov5env source yolov5env/bin/activate # Windows下用 yolov5env\Scripts\activate git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt

如果pip下载慢,可以在requirements.txt安装命令后追加-i https://pypi.tuna.tsinghua.edu.cn/simple,一般几分钟能装完。这个步骤最常见的坑是CPU版PyTorch被装上来,检查方式是在终端里运行python -c "import torch; print(torch.cuda.is_available())"。如果输出False,说明CUDA不可用,需要去PyTorch官网按你的CUDA版本重新安装torch。GPU训练交通标志确实比CPU快很多,但不代表CPU不能训,只是epochs在CPU上跑一天都很难跑完,节奏会被拖垮。

3.2 改数据配置和模型配置:nc参数决定检测头输出

在yolov5目录下新建一个data/tt100k.yaml,内容指向你刚才组织好的数据目录。注意路径写成绝对路径或相对YOLOv5目录的路径,别写~/,YOLOv5对中文路径和带空格路径支持不好,会在加载图片时报编码错误。

# data/tt100k.yaml train: datasets/tt100k/images/train val: datasets/tt100k/images/val nc: 6 # 类别数必须和转换脚本里的classes长度一致 names: 0: i2 1: i4 2: i5 3: i6 4: pl40 5: pl80

同时需要修改模型配置文件。YOLOv5提供了yolov5s.yaml、yolov5m.yaml、yolov5l.yaml等不同大小,它们对应不同的网络深度和宽度。如果你看过YOLOv5网络结构图,会注意到检测头每个尺度的输出通道数和类别数相关,所以必须把yolov5s.yaml里的nc改成你的实际类别数。可以用sed命令直接改:

sed -i 's/nc: 80/nc: 6/' models/yolov5s.yaml

nc从80改成6后,检测头输出张量的通道数会从255变成18,也就是3个anchor乘以每个框的5个坐标加分类数。如果这里不改,训练时会直接报“shape mismatch”的错误。对于交通标志这种小目标场景,我一般用yolov5s起步,它参数量小,训练一轮时间短,方便频繁调参;等流程跑通后再换成yolov5m或yolov5l看上限。

3.3 训练命令与超参数设置:不要盲目抄别人的epochs

数据配置和模型配置改好后,开始训练。最简单的命令如下:

python train.py \ --weights yolov5s.pt \ --data data/tt100k.yaml \ --epochs 150 \ --batch-size 16 \ --imgsz 640 \ --device 0

--weights yolov5s.pt会下载一个在COCO上预训练好的权重,建议保留。虽然COCO里没有交通标志这一类别,但预训练模型的backbone已经学到通用边缘、纹理和颜色特征,能显著缩短训练收敛时间。如果网络质量不好,权重下载不下来,也可以不传这个参数从头训练,但loss会下降得非常慢。--device 0指定第一张显卡;没有GPU时改成--device cpu,同时把--batch-size降到8甚至4。

常见参数的含义和调整思路如下表:

参数作用交通标志场景建议
--epochs训练总轮数先跑100轮看曲线,CCTSDB用100,TT100K子集用150-200
--batch-size每步喂入的图数显存不足时减半,不要低于4
--imgsz训练输入尺寸默认640,如果标志平均尺寸小于32像素,可试960
--hyp超参数文件路径修改hyp.scratch-low.yaml中的lr0、mosaic等
--cache是否缓存图片到内存小数据集开启能减少IO等待,但吃内存

YOLOv5的超参数文件控制着学习率、数据增强概率、损失权重等,比改网络结构更直接。对交通标志来说,最重要的两个超参数是lr0和mosaic。lr0默认0.01,如果训练曲线震荡很厉害,可以降到0.005;mosaic默认1.0,即100%使用马赛克增强,它把四张图拼成一张,对检测小目标有帮助,但如果你选了TT100K子集且类别很少,马赛克反而会让标志拼接变形,验证集mAP上不去,可以把mosaic设成0.5试试。

另外要提醒一个经常被忽略的参数:--project和--name。YOLOv5每次训练结果会保存在runs/train/exp下,多次训练会生成exp2、exp3。我习惯用--name tt100k_lr005这种带参数含义的名字,否则三天后你根本不知道exp7是哪次训练得来的,调参时无法对照。

4. 模型评估与训练排查:六个让交通标志mAP上不去的常见原因

训练交通标志模型的过程,说穿了是在和数据集、超参数、显存容量这三样东西搏斗。下面这六个问题是我在毕设和后来帮人调模型时翻车最频繁的,每一条都按现象、原因、解决来讲。

4.1 只看mAP不看P/R曲线和混淆矩阵:现象、原因、解决

训练结束后,很多人的习惯是看一眼终端最后的mAP,超过0.8就觉得万事大吉。现象是答辩演示时,模型对“限速40”的召回很差,但mAP数字又很高,因为mAP被大量“直行”样本拉高了。原因是mAP是按类别算平均,被多数类主导,少数类表现差不会明显拉低总分。解决方法是打开runs/train/tt100k_lr005/results.png和confusion_matrix.png,逐类看Precision和Recall。我在训练完成后会写一个小脚本统计每一类的AP值,YOLOv5的val.py在验证后会在终端打印详细的类别指标,但更容易的方式是看验证阶段保存的PR_curve.png,哪一类的曲线包围面积明显小,就是那个类别踩坑了。

4.2 小目标交通标志漏检:现象、原因、解决

现象是训练loss很低,但模型对远处路口的“禁止驶入”标志总是漏检,框出来的位置偏大且置信度低。原因是YOLOv5下采样5次后,原图32x32的物体在特征图上只有1x1像素,携带的语义信息太少。解决思路有三个,按成本从低到高排:第一,把--imgsz从640提到960,输入分辨率变大后小目标在特征图上占更多像素,但显存占用会提升一倍以上;第二,在配置文件里增加浅层的anchor数量,YOLOv5默认每个尺度3个anchor,你可以把yolov5s.yaml中的anchors改成4x,使小尺度的anchor更密;第三,如果这些都不行,就用切图推理——把一张1920x1080街景图切成1280x1280两块分别检测,再合并结果。切图能立竿见影,但要注意切边上的目标被截断,需要保留重叠区域并在后处理中用NMS合并重复框。

4.3 训练集指标高、真实场景变差:现象、原因、解决

现象是验证集mAP有0.9,拿手机随便拍一张道路照片检测,框的位置飘且误检多。原因是数据划分不当导致模型记住了训练集中背景的纹理,真实场景与街景颜色分布不一致。另外TT100K是从行车记录仪视角截取的,几乎没有夜间、逆光和雨天的样本。解决时我先检查数据划分,用第2章提到的按文件前缀分组方式重分;然后增加在线数据增强,在hyp.scratch-low.yaml里把hsv_h、hsv_s、hsv_v适当调大或者开启degrees让图像小角度旋转,模拟不同拍摄姿态。注意不要为了提升增强强度把所有项都拉满,否则标志会发生语义扭曲,比如“限速80”的“8”被转成“3”。

4.4 类别不平衡导致个别类mAP为0:现象、原因、解决

现象是训练到一半,terminal里每个epoch打印的验证集mAP中,pl80类一直是0.0,但总mAP在涨。原因是TT100K中pl80的样本数可能只有几十个,而i2有几千个,一个batch里几乎见不到pl80,模型在反向传播时对该类别的梯度贡献微乎其微。解决方式最直接的是做类别筛选,只留下每类样本数超过200的类别;如果为了答辩必须保留某一类,就用离线复制加轻微模糊、缩放来扩样,注意别把同一张图的副本全部放进验证集。还有一种做法是为少数类提高损失权重,但YOLOv5官方训练脚本没有独立的类别权重开关,需要修改loss函数,这对毕设来说过于复杂,不如直接扩样本。

4.5 显存溢出或训练中断:现象、原因、解决

现象是训练开始时显存占满,报CUDA out of memory;有时夜里训练中断,第二天想继续却不知从哪开始。OOM的解决套路很固定:把--batch-size减半,把--imgsz从960降到640,同时关闭--cache。如果还想用大图训练,可以在代码里开启梯度累积,但YOLOv5命令没有直接提供这个参数,需要改train脚本,不建议新手碰。训练中断的后悔药是--resume,YOLOv5每次训练都会在runs/train/exp目录里保存last.pt,下次执行时带上--resume就能接着上次进度继续,不需要重新载入epochs。注意--resume要配合原来的数据和超参文件,否则权重和训练状态对不上,会静默地从第0轮重新开始但学习率被重置,导致曲线跳变。

4.6 标注文件错了半天看不出来:现象、原因、解决

现象是训练时loss在某个epoch后突然上升,或者模型输出了许多置信度很高但完全不对的框。原因是转换脚本里类别映射写反了,或者有些图像存在空标签但训练脚本没有提示。解决方法是先把标签可视化,不要用模型,只是把txt画回图上。下面这个脚本可以快速检查单张图:

import cv2 import numpy as np import os img_path = 'datasets/tt100k/images/train/xxx.jpg' label_path = img_path.replace('images', 'labels').replace('.jpg', '.txt') img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: parts = line.strip().split() cls = int(parts[0]) x_center, y_center, bw, bh = map(float, parts[1:]) x1 = int((x_center - bw / 2) * w) y1 = int((y_center - bh / 2) * h) x2 = int((x_center + bw / 2) * w) y2 = int((y_center + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imwrite('check.jpg', img)

这种检查能发现两类问题:一类是框宽度为0或为负,说明转换时坐标顺序写反;另一类是类别ID和图上物体明显不匹配,比如一辆自行车上标着pl80。我通常抽50张图批量生成,放一个文件夹里快速翻一遍,视觉检查比任何指标都更能暴露数据问题。

5. 把训练好的交通标志模型用起来:ONNX导出、摄像头推理与轻量化顺序

训练出一版满意的模型只是第一步。毕业设计的加分项在于你能把模型从train.py里拿出来,用摄像头实时检测,并跑到嵌入式设备上。

先导出成ONNX,方便后续用OpenVINO或ONNXRuntime做CPU推理:

python export.py \ --weights runs/train/tt100k_lr005/weights/best.pt \ --include onnx \ --imgsz 640

导出后用下面的推理命令验证:

python detect.py \ --weights runs/train/tt100k_lr005/weights/best.pt \ --source 0 \ --conf-thres 0.4 \ --iou-thres 0.5

--source 0表示打开默认摄像头。如果你打算做树莓派或Jetson上的实时识别,就需要考虑轻量化。YOLOv5后处理里的NMS在CPU上比较耗时,可以先开启--half让权重转FP16,或者把--img 320减小输入尺寸。看到网上很多人在RK3568这类NPU上做量化部署,你如果只是毕设展示,做到ONNX加OpenVINO推理已经足够;如果还想加量化,注意把归一化和Resize算子留在模型里,否则NPU上每个输入都要单独预处理,容易被卡住。

我实践中最深的教训是,做交通标志识别不要只对着数据集图片调参。毕业设计答辩前,我花了大量时间提升mAP,却在现场用手机拍窗外道路时,模型把红色圆形广告牌识别成了禁止标志。后来我把训练集里加入了一些相似的干扰物图片作为负样本,并且在检测时设置--conf-thres 0.45而不是默认0.25,误检才明显减少。调完模型后一定要保留三张没参与过训练的真实拍摄照片作为测试底线,如果这三张都能检出且不误检,再谈性能和指标。希望这个思路能帮到你。

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

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

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

立即咨询