基于YOLOv5的交通标志识别检测:从数据集处理到模型训练实战

📅 发布时间:2026/10/8 23:21:08
基于YOLOv5的交通标志识别检测:从数据集处理到模型训练实战
简介面向高校毕业设计场景的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适合类别太少展示效果不够细TT100K100JSON需要筛选转换类别不平衡小目标多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和bboxbbox是最小外接矩形的四元组。下面脚本可以完成转换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_okTrue) with open(json_path, r, encodingutf-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), fdatasets/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), fdatasets/tt100k/labels/train/{label_name}) for key in val_keys: for fname in groups[key]: shutil.copy(os.path.join(train, fname), fdatasets/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), fdatasets/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和多个objectobject里的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_okTrue) 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.yamlnc从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用100TT100K子集用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.005mosaic默认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脚本不建议新手碰。训练中断的后悔药是--resumeYOLOv5每次训练都会在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误检才明显减少。调完模型后一定要保留三张没参与过训练的真实拍摄照片作为测试底线如果这三张都能检出且不误检再谈性能和指标。希望这个思路能帮到你。本文还有配套的精品资源点击获取