简介:这份资源是一套基于YOLOv5的道路交通标志识别完整项目,主要面向毕业设计、期末大作业及课程设计等场景,也适合对目标检测感兴趣的初学者参考。项目包含Python源码与配套数据集,代码中附有注释,从数据处理、模型训练到推理识别均有覆盖,方便本地部署后直接运行。资源包共266个文件,压缩后大小约423MB,文件类型涵盖py核心代码、pt模型权重、yaml配置文件、jpg/jpeg图像样本、sh脚本、csv训练记录及Dockerfile等,结构与用途较为清晰。已有439人浏览学习。内容包含训练日志、results.csv指标变化、Docker部署文件与部分配置文件,便于了解训练过程和环境搭建方式。对希望快速完成高分成品项目的学生而言,这套资源提供了一条可复用的完整实现路径,可在短时间内上手并用于演示或答辩。
1. 从毕业设计题目到可运行系统:yolov5道路交通标志识别要交付的不只是源码
打开资源站搜“基于yolov5道路交通标志识别程序源码+数据集(毕业设计)”,下载包里基本都是同一套东西:yolov5源码、一堆标志图片、标注文件、一份说明文档。很多同学以为解压就能跑,结果卡在环境配置、数据集转换和训练参数上。这个题目真正考验的,是把yolov5源码、道路交通标志数据集、训练脚本和演示界面串成一条可复现的工程链路。道路交通标志识别和普通目标检测最大的区别在于标志属于典型小目标,一个1920×1080画面里标志可能只有40×40像素,再加上夜间反光、遮挡和类别不均衡,很容易出现“训练mAP高、现场识别差”的情况。这个方案适合正在做毕业设计、需要快速产出可运行系统的学生,也适合想验证yolov5训练自己数据集全流程的工程师。
2. 建数据集与目录结构:从COCO2017到yolov5训练自己的数据集
2.1 不要直接拿COCO2017当训练集:交通标志数据集怎么选
很多教程一上来就让人下载COCO2017,理由是yolov5官方预训练权重在COCO上训练过。但COCO2017数据集结构是面向80类通用目标的,train2017里有118K张图、val2017有5K张图,标注存放在annotations/instances_train2017.json等文件里。这套结构里确实有traffic_sign类别,但实例数量少,而且标志类别只占很小一部分。直接用COCO训练交通标志识别,等于让模型在大量无关背景里找少量小物体,收敛慢、误检也多。
更合适的做法是换用专用数据集。常见的有TT100K(清华大学-腾讯公开的交通标志数据集)、CCTSDB(长沙理工的交通标志数据集)、GTSRB(德国交通标志数据集)。TT100K标注了街景图片里的标志实例,类别多,但部分类别样本极少;CCTSDB是国内场景,类别相对集中;GTSRB虽然主要用于分类,但也可以转成检测数据来扩充不同光照下的样本。下载时尽量去学校主页或论文主页找原始包,资源站打包经常出现图片被压缩、标注缺失、类别名被改成数字的问题。
我一般建议毕业设计选一个数据集做主体,不要贪多。比如用TT100K里的停止、限速、禁止通行等5到10个常用类别,再补几十张自己手机拍的路口照片。这样标注量控制在1000张左右,训练时间和答辩讲稿都好写。如果直接用官方COCO做迁移,反而要处理类不平衡和标注过滤,工作量会大很多。
2.2 把VOC/JSON标注转成YOLO格式:转换脚本与参数说明
yolov5训练自己的数据集要求每张图片对应一个同名txt文件,每行格式是:
class_id x_center y_center width height坐标是归一化到0到1的浮点数,x_center和y_center是框中心点,width和height是框的宽高。这个格式和VOC的xmin、ymin、xmax、ymax不一样,和COCO的bbox也不一样。下面这段代码把VOC XML标注转成YOLO txt:
import os import glob import xml.etree.ElementTree as ET # 类别顺序必须和后续data.yaml里的names保持一致 CLASSES = ['spd_30', 'spd_60', 'stop', 'crosswalk'] def xml_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) with open(os.path.join(out_dir, os.path.basename(xml_path).replace('.xml', '.txt')), 'w') as f: for obj in root.iter('object'): name = obj.find('name').text if name not in CLASSES: continue cls_id = CLASSES.index(name) bndbox = obj.find('bndbox') x1 = float(bndbox.find('xmin').text) y1 = float(bndbox.find('ymin').text) x2 = float(bndbox.find('xmax').text) y2 = float(bndbox.find('ymax').text) x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h # 防止坐标越界,标注框偶尔会超出图片边界 x_center = min(max(x_center, 0), 1) y_center = min(max(y_center, 0), 1) w = min(max(w, 0), 1) h = min(max(h, 0), 1) f.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n") os.makedirs('labels', exist_ok=True) xml_files = glob.glob('xml/*.xml') for xml_file in xml_files: xml_to_yolo(xml_file, 'labels') print('converted', len(xml_files), 'files')这段代码的核心逻辑是:先读图片真实宽高,再把VOC的绝对坐标换算成相对坐标。CLASSES列表的顺序决定每个类别的数字id,这个顺序在后续data.yaml里不能再变。代码里做了0到1的钳制,但遇到x2小于x1这种标注错误时钳制没用,需要额外报错。
需要注意,VOC XML里size子节点必须有宽高,有些公开数据集漏写了size节点,会导致img_w拿不到值。转换前最好先统计一下XML里有多少个文件缺size字段,缺了就直接删掉对应图片,避免训练到一半报错。
2.3 目录结构、数据划分和标签自检
yolov5约定的数据集目录结构一般长这样:
datasets/ ├── traffic_sign/ │ ├── images/ │ │ ├── train/ │ │ │ ├── img_001.jpg │ │ │ └── ... │ │ └── val/ │ │ └── ... │ ├── labels/ │ │ ├── train/ │ │ │ ├── img_001.txt │ │ │ └── ... │ │ └── val/ │ │ └── ... │ └── data.yaml图片和标签要严格同名,只是扩展名不同。yolov5依赖文件名匹配图片和标签,如果从网上下载的数据集文件被重命名过,最好用脚本统一改成img_00001.jpg这类定长编号,避免文件名里有空格或中文。下面是数据划分脚本,把all目录里的图片和标签按15%随机分成验证集:
import os import random import shutil random.seed(42) src_images = 'datasets/traffic_sign/images/all' src_labels = 'datasets/traffic_sign/labels/all' train_img_dir = 'datasets/traffic_sign/images/train' train_lbl_dir = 'datasets/traffic_sign/labels/train' val_img_dir = 'datasets/traffic_sign/images/val' val_lbl_dir = 'datasets/traffic_sign/labels/val' for d in [train_img_dir, train_lbl_dir, val_img_dir, val_lbl_dir]: os.makedirs(d, exist_ok=True) img_files = [f for f in os.listdir(src_images) if f.lower().endswith(('.jpg', '.jpeg', '.png'))] random.shuffle(img_files) val_count = max(int(len(img_files) * 0.15), 1) val_files = set(img_files[:val_count]) for img_name in img_files: stem = os.path.splitext(img_name)[0] label_src = os.path.join(src_labels, stem + '.txt') if not os.path.exists(label_src): print(f'warning: missing label for {img_name}') continue max_len = max(len(img_files) - val_count, 0) # 简单进度提示 if img_name in val_files: shutil.move(os.path.join(src_images, img_name), os.path.join(val_img_dir, img_name)) shutil.move(label_src, os.path.join(val_lbl_dir, stem + '.txt')) else: shutil.move(os.path.join(src_images, img_name), os.path.join(train_img_dir, img_name)) shutil.move(label_src, os.path.join(train_lbl_dir, stem + '.txt')) print(f'moved {img_name}, remaining {len(img_files) - 1}') # 这里只作示意脚本固定随机种子,保证复现。划分前先检查标签是否存在,避免训练时出现“找不到标签”的报错。移动完成后,在datasets/traffic_sign/下建data.yaml:
path: datasets/traffic_sign train: images/train val: images/val nc: 4 names: ['spd_30', 'spd_60', 'stop', 'crosswalk']path可以写相对路径,也可以写绝对路径;如果在Windows和Linux之间反复切换,建议直接用绝对路径,反斜杠问题会少很多。nc必须和names列表长度一致,names里的顺序必须和转换脚本里CLASSES的顺序一致。这里如果顺序乱了,训练不会报错,但预测出来的类别id会对应到错误的标识。
COCO2017数据集结构使用的是instances JSON,类别id存在categories字段里,和yolov5的txt格式不同。如果硬要从COCO转,需要过滤出交通标志类别并重新映射id,过程不复杂但容易出错。对毕业设计来说,直接用交通标志专用数据集再转YOLO格式,效率高得多,也方便自己控制类别。
3. 用yolov5源码跑通训练:超参数、模型选型与训练命令
3.1 yolov5环境配置与源码获取
yolov5环境配置最常见的坑是torch版本和CUDA不匹配。官方requirements.txt里给的是最新torch,如果你的显卡驱动比较旧,pip装出来的torch可能不识别CUDA。我一般先用conda建独立环境:
conda create -n yolo python=3.9 -y conda activate yolo pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --index-url https://download.pytorch.org/whl/cu117 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt然后验证torch能不能正常调用显卡:
python -c "import torch; print(torch.cuda.is_available())"如果是True,再跑python train.py --help确认yolov5源码能正常加载。如果环境里没有git,直接去GitHub下载zip包也是一样的,后面所有命令都在解压后的yolov5目录下执行。需要注意,yolov5版本不同,训练命令里参数名略有差异,比如老版本用--cfg指定模型配置,新版本改成在yaml里写模型配置。写代码时尽量锁定一个版本,毕业设计不需要追新。
3.2 修改数据配置与模型配置
数据配置在data.yaml里已经写好了,接下来是模型配置。yolov5默认提供yolov5n、yolov5s、yolov5m、yolov5l、yolov5x五档模型,后缀越小模型越轻量。交通标志类别少,yolov5s够用,如果推理设备是CPU或老显卡,选yolov5n。复制一份模型配置,把nc改成自己的类别数:
cp models/yolov5s.yaml models/yolov5s_traffic.yaml然后修改models/yolov5s_traffic.yaml,核心部分大概是:
nc: 4 depth_multiple: 0.33 width_multiple: 0.50 anchors: - [10,13, 16,30, 33,23] - [30,61, 62,45, 59,119] - [116,90, 156,198, 373,326]nc必须和data.yaml的nc一致。depth_multiple和width_multiple控制网络层数和通道数,这是yolov5s与yolov5m的区别,毕业设计先不动。anchors是预设的anchor尺寸,默认从COCO聚类得到。训练自己数据集时yolov5会尝试重新聚类,如果想手动干预,训练命令里可以加--noautoanchor,但除非你已经看过聚类结果,否则不建议一开始就关。
3.3 超参数与训练参数:怎么调才不玄学
训练命令是整套方案里最核心的一步:
python train.py \ --data datasets/traffic_sign/data.yaml \ --cfg models/yolov5s_traffic.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --imgsz 640 \ --name traffic_sign_run \ --device 0--data指向数据集配置,--cfg指向模型配置,--weights使用官方COCO预训练权重,这样能明显加快收敛。--device 0指定第一张显卡,如果是CPU训练就不加这个参数,速度会慢很多。--imgsz 640是网络输入尺寸,不是原图尺寸。交通标志在街景里往往偏小,把imgsz提高到1280能改善小目标检测,但训练显存和时间都会涨,需要一个平衡。
常见参数建议如下表:
| 参数 | 建议值 | 说明 |
|---|---|---|
| batch-size | 8到16 | 显存不足时优先降到4 |
| epochs | 100到150 | 数据量少时100轮足够 |
| imgsz | 640或1280 | 小目标明显时用1280 |
| workers | 4到8 | OOM时可以设0排查 |
| optimizer | SGD | 默认即可,稳定 |
| lr0 | 0.01 | 迁移学习常驻默认值 |
| mosaic | 1.0 | 小目标增强有效,过度裁切会引入脏样本 |
超参数文件在data/hyps/hyp.scratch-low.yaml。毕业设计阶段没必要把所有超参数都改一遍,只关注lr0、lrf、mosaic这几个就行。lr0是初始学习率,迁移学习用0.01很稳;lrf是最终学习率与初始学习率的比值,默认0.2,我习惯改成0.1,让训练后期更细腻;mosaic是马赛克增强,对小目标有帮助,但如果你的标注框很小,mosaic裁切后可能只剩半个物体,可以降到0.5。
3.4 训练输出与效果评估
训练结束后,结果在runs/train/traffic_sign_run/下。best.pt是验证集mAP最高的权重,last.pt是最后一次epoch的权重,部署时优先用best.pt。results.png里有loss曲线和mAP曲线,答辩时可以直接放这张图说明收敛过程。
光看最终mAP不够,要检查验证集loss是否跟着下降。如果train loss一直在降但val loss降不下去,说明过拟合,优先补数据增强或换小模型。如果mAP@0.5只有0.2左右,不要盲目加epoch,先回退去看标签:可能是类别顺序错了,也可能是标注框过小,yolov5把大量小框当噪声滤掉了。
训练日志里每一轮会打印box_obj、cls、obj几个loss分量。如果cls占比异常高,说明类别学习存在问题,检查data.yaml的names顺序。如果obj一直不降,大概率是anchor和物体尺寸不匹配。遇到这种情况,先看runs/train/traffic_sign_run/anchors.png,再决定是否手动调整anchor。
4. 从模型到演示系统:推理、导出与界面跑通
4.1 用detect.py跑图片和视频推理
训练完成后的常规操作是用detect.py验证效果。命令写法如下:
python detect.py \ --weights runs/train/traffic_sign_run/weights/best.pt \ --source data/images/street.jpg \ --conf-thres 0.25 \ --iou-thres 0.45 \ --save-txt \ --project runs/detect \ --name traffic_demo--source可以是图片、文件夹、视频文件,也可以是摄像头设备号0。如果现场演示没有摄像头,用一段路口视频是最稳妥的选择,帧率不会因为IO变慢。--conf-thres是置信度阈值,0.25是默认值;如果检测结果里漏框多,可以降到0.15,但误检会增加。--iou-thres是NMS的IoU阈值,0.45常规不用动。--save-txt会把每个目标的框坐标和类别存成txt,这个比较好用,后续做红灯统计、标志数量统计都从txt里读。
跑完在runs/detect/traffic_demo/下查看标注图片。如果发现检测框位置正确但置信度普遍低于0.3,说明训练数据里的标志样本太小,或者训练时imgsz不够大。此时可以先试imgsz=1280重新训练,数据增强也适当加强。
4.2 导出ONNX与部署权衡
毕业设计如果只做训练和detect.py演示,工作量偏单薄。更常见的做法是导出ONNX,再单独写推理程序。导出命令非常简单:
python export.py --weights runs/train/traffic_sign_run/weights/best.pt --include onnx --imgsz 640导出后会在权重同目录生成best.onnx。ONNX模型可以用onnxruntime加载,不需要再安装完整的yolov5训练环境:
pip install onnxruntime-gpu python -c "import onnxruntime as ort; print(ort.get_available_providers())"这里要注意,onnxruntime-gpu同样有CUDA版本匹配问题,如果训练机的CUDA版本和onnxruntime依赖不一致,运行时会出现“CUDA execution provider”加载失败。不想折腾的话就用CPU版onnxruntime,检测一张640×640的图片在普通台式机上只要几百毫秒,毕业设计演示完全够用。
导出ONNX只是第一步,最后一步是用PyTorch直接加载权重写推理逻辑,因为PyTorch调用对大多数学生更熟悉,也省去NMS后处理的麻烦。树莓派或嵌入式设备上部署时,再考虑ONNX或OpenVINO,模型可以换成yolov5n。
4.3 给答辩和用户看的轻量识别界面
很多毕业设计要求有界面,常见的做法是Flask做个网页,上传图片就能显示检测框。核心推理函数可以这样写:
import cv2 import torch def run_inference(model, img_path, conf=0.25): img = cv2.imread(img_path) results = model(img, conf=conf) boxes = results.xyxy[0].cpu().numpy() # 每行: x1, y1, x2, y2, conf, cls_id for b in boxes: cls_id = int(b[5]) label = f"{model.names[cls_id]} {b[4]:.2f}" cv2.rectangle(img, (int(b[0]), int(b[1])), (int(b[2]), int(b[3])), (0, 255, 0), 2) cv2.putText(img, label, (int(b[0]), int(b[1]) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) return img这里的results.xyxy[0]返回的是像素坐标,直接画框就行,不需要再归一化。model.names来自训练时的data.yaml,所以如果names是中文,OpenCV的putText画出来会乱码。稳妥做法是names里存英文或拼音,展示时再映射到中文。
Flask部分只需要把函数包成一个路由,接收上传图片,返回标注后的图片。前端不需要复杂,一张表格加一个img标签就能满足答辩演示。重点是把推理逻辑和训练过程讲清楚,界面是加分项,不是核心。
5. 避坑与排查:毕业设计里最常见的5个翻车现场
5.1 训练一开始loss就是nan
现象:训练第一轮loss直接显示nan,然后一直不恢复,训练基本作废。翻车原因有两个高频点:一是标签txt里出现了负坐标或宽高为0,二是类别id超过了nc-1,导致loss计算越界。很多公开数据集的标注本身不干净,转换脚本没有过滤是主因。
解决:训练前扫描一遍所有标签文件,把宽高小于等于0的行删掉,把坐标异常的样本挑出来重标。也可以写个几行脚本检查每行字段数量,正常情况下每行必须是5个值:类别id、中心点x、中心点y、宽、高。
import os for split in ['train', 'val']: label_dir = f'datasets/traffic_sign/labels/{split}' if not os.path.exists(label_dir): continue for f in os.listdir(label_dir): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f)) as fp: for line in fp: parts = line.split() if len(parts) != 5 or float(parts[3]) <= 0 or float(parts[4]) <= 0: print('bad label', split, f, line.rstrip())这条检查做完,再跑短epoch的小批量训练,确认loss能降,再上全量。
5.2 显存不足:只调batch-size没用
现象:6G显存跑batch-size=16直接OOM,改成8还是OOM,换batch-size=4也只是晚崩溃几分钟。实际上显存占用大头除了batch-size,还有输入尺寸和模型深度。--imgsz 640比--imgsz 416的显存占用增加接近2倍;yolov5s虽然是轻量模型,但梯度回传时的中间激活值在batch-size较大时仍然可能爆显存。
解决:先固定imgsz=640,把batch-size降到4,再不行就换yolov5n模型配置。训练时可以同时设--workers 0,虽然数据加载慢一点,但能排除数据预取导致的额外显存占用。如果毕业设计只有一台8G甚至6G显存的笔记本,建议直接用yolov5n,训练时间和显存压力都会小很多。
5.3 mAP高但现场识别不准:小目标anchor没有重聚类
现象:验证集mAP@0.5有0.8,但拿一张真实路口原图进去,交通标志全没框出来,把图片放大后模型又能正确识别。原因很典型:默认anchor是从COCO数据集聚类出来的,COCO里大中目标多,交通标志在街景中占比小,模型在训练时会重新聚类anchor,但数据里小目标数量不够,聚类结果仍然偏向中等尺度。
解决:训练结束后打开runs/train/traffic_sign_run/anchors.png,对比红蓝点分布。如果标注框尺寸明显小于默认anchor第一组,可以把models/yolov5s_traffic.yaml里的anchors改成更小的一组,比如[4,5, 8,10, 12,18],同时把训练imgsz提高到1280。验证时也要用同样的imgsz,不然模型尺度不匹配,推理效果会打折扣。
5.4 中文标签画成乱码
现象:自写推理脚本里用cv2.putText绘制“停止标志”,输出图片上全是问号,或者什么都不显示。OpenCV自带的putText只支持英文字体和数字,不支持中文。模型预测的label来自names列表,如果names直接写中文,训练本身没问题,但绘图阶段会翻车。
解决:最简单的方法是在data.yaml里使用英文或拼音,例如stop、speed_limit_60,绘图时再做一个字典映射到中文。如果必须在图片上显示中文,用PIL加载中文字体,比如Windows下的C:/Windows/Fonts/msyh.ttc,把文字渲染到临时图像上,再贴回原图。答辩演示中英文标签也是可以被接受的,别在这个点上浪费时间。
5.5 torch.cuda.is_available()返回False
现象:环境配置完成后,训练时打印警告“CUDA unavailable”,只能用CPU慢慢跑。原因大多是pip安装torch时默认装了CPU版本,或者torch版本与显卡驱动不匹配。很多人看到requirements.txt里有torch就直接pip install,结果装成了不带CUDA的wheel。
解决:先卸载已有torch和torchvision,再按PyTorch官网指引安装对应CUDA的wheel。以CUDA 11.7为例:
pip uninstall -y torch torchvision pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --index-url https://download.pytorch.org/whl/cu117装完再验证一次torch.cuda.is_available(),如果还是False,检查显卡驱动版本是否支持目标CUDA。这一步是所有后续训练的地基,值得花半小时踩平。
6. 进阶玩法:把超参数搜索和模型轻量化写进毕设
6.1 用yolov5自带的超参数进化脚本跑一轮
yolov5训练脚本内置了超参数进化功能,通过--evolve参数搜索一组mAP更高的超参数。在数据量不大时,可以先跑一个小代次搜索当作亮点:
python train.py \ --data datasets/traffic_sign/data.yaml \ --cfg models/yolov5s_traffic.yaml \ --weights yolov5s.pt \ --epochs 20 \ --batch-size 8 \ --evolve 50 \ --cache--evolve 50表示搜索50组超参数组合,每组用20个epoch评估。这个命令在普通笔记本上可能要跑几个小时,毕业设计如果不追求极致效果,直接用默认hyp文件就够了。想手动调的话,我最常用的是把lrf改成0.1,让学习率在训练后期衰减更缓慢,对小数据集更友好。
6.2 把yolov5s换成yolov5n或导出TensorRT
如果答辩现场要演示摄像头实时识别,而机器没有独立显卡,yolov5n是最稳妥的选择。训练命令不需要大改,把--weights yolov5n.pt和--cfg models/yolov5n_traffic.yaml替换掉即可。yolov5n的参数只有yolov5s的三分之一不到,CPU推理速度能快一倍,代价是精度会低一些。
如果手里有NVIDIA显卡,可以导出TensorRT引擎进一步加速:
python export.py --weights runs/train/traffic_sign_run/weights/best.pt --include engine --imgsz 640 --device 0TensorRT的整合成本比ONNX高不少,如果只是答辩演示,ONNX加onnxruntime已经够了。我的习惯是:拿到一个新的数据集,先把标签检查脚本跑一遍,再用十张图片做个冒烟训练,确认loss正常后才会跑全量。这套流程帮我避开了很多次标注文件翻车。希望帮到你。
本文还有配套的精品资源,点击获取