简介:基于YOLOv5的交通标志牌识别项目,采用TT100K数据集,代码经测试可正常运行,适合毕业设计、课程设计、课程大作业等场景,尤其面向计算机相关专业的学生或从业者,适用技能从入门到进阶。压缩包结构清晰,文件总数达一百四十七份,体积约一点一兆,其中包含五十余个用Python编写的源代码文件,以及大量yaml格式的配置与标注文件,同时提供Shell脚本、markdown说明文档、Jupyter演示和Docker镜像支持,覆盖数据处理、模型配置、训练验证与容器化部署等完整流程,方便按需查找和使用。目前已有三百五十二人学习,可直接下载用于实战练习,无论是初次接触目标检测的小白,还是需要完成课程作业或毕设展示的同学,都具有较高借鉴价值。从代码结构来看,模块划分合理,支持修改参数并迁移到其他检测任务;配合项目说明文档,可帮助理解YOLOv5在交通标志场景下的数据加载、训练与推理流程,也可作为课设或毕设的完整基础框架,能显著节省环境准备与代码调试时间。
1. 基于yolov5的交通标志牌识别:这份源码包到底能跑出什么
做过毕设或课设的人都知道,yolov5到今天依然是目标检测领域最容易落地、教程最多、踩坑资料最全的一条路线。这份资源把官方yolov5仓库和TT100K交通标志数据集打包在一起,形成一个「开箱即跑」的交通标志牌识别项目:训练代码、推理代码、数据转换逻辑、项目说明都齐了,你拿到手不是去读一篇空泛的原理文章,而是直接能把一个检测模型从数据准备跑到权重导出。它能解决的是那种「论文写得出来、代码跑不起来」的尴尬——适合计算机相关专业学生做毕业设计、课程设计,也适合刚入门目标检测的开发者拿真实数据集练手。TT100K里全是中国的交通标志,标志小、背景杂、类别多,拿来练手比用COCO更贴近实际场景,也更有说服力。
2. 跑通项目之前:环境、目录与第一次推理
2.1 环境版本:python、torch、CUDA 怎么配对不翻车
先确认一件事:yolov5对版本组合比较敏感,不是「最新版一定最好」。这个项目目录里保留了官方仓库大量历史文件,按文件结构判断,项目骨架对应的是 yolov5 v6.0 之后的版本,这套版本对 python 3.8/3.9/3.10 支持最好,torch 1.10 到 2.x 都能跑。我的建议是不要追新,直接按官方 requirements.txt 锁版本。
先创建一个干净的虚拟环境,避免把系统python搞乱:
conda create -n yolov5 python=3.9 conda activate yolov5 cd 项目根目录 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple这段命令的用意是:用 conda 单独建一个 python 3.9 环境,再把项目依赖一次性装进去。后面那个-i参数是国内镜像源,如果你网络环境访问 PyPI 快,可以去掉。注意不要在 base 环境里直接装,yolov5 依赖的 torch、opencv、matplotlib 版本和其他项目经常打架,隔离环境是省时间的第一个习惯。
接下来是 CUDA 的问题。如果你有 N 卡,先看显卡驱支持的最高 CUDA 版本,再决定装哪个 torch:
nvidia-smi python -c "import torch; print(torch.cuda.is_available())"nvidia-smi右上角会显示驱动支持的 CUDA 版本,比如 12.1,那你就装配套的 torch。常见组合是 CUDA 11.8 配 torch 1.13.1,或者 CUDA 12.1 配 torch 2.1.0。如果torch.cuda.is_available()返回 False,说明 torch 是 CPU 版,需要重装 CUDA 版。这一步是新手最容易卡住的地方,后面的所有步骤都依赖这个检测结果,值得花五分钟确认。
2.2 项目目录拆解:train.cache、CITATION.cff、Dockerfile 各自干什么
打开 zip 包,你会发现根目录下有不少一眼看不懂的文件。这些不是冗余,是 yolov5 官方仓库的标准配置,只是大部分教程不会逐个讲。
| 文件 | 作用 | 常用场景 |
|---|---|---|
| train.cache | 训练数据缓存索引,记录图片路径和标签校验结果 | 训练启动时自动生成/读取,文件损坏会报错 |
| CITATION.cff | 学术引用格式声明 | 论文引用、GitHub 自动识别作者信息 |
| setup.cfg | 打包与 lint 配置 | 执行pip install -e .本地安装时生效 |
| Dockerfile | 默认镜像构建文件 | GPU 环境容器化部署 |
| Dockerfile-arm64 | ARM 架构镜像构建文件 | 树莓派、ARM 服务器部署 |
| Dockerfile-cpu | 纯 CPU 版镜像构建文件 | 无 GPU 环境跑推理 |
| .dockerignore | Docker 构建时排除的文件清单 | 避免把缓存和数据集打进去 |
| .gitattributes | Git 行尾与 LFS 配置 | 跨平台克隆时避免行尾符混乱 |
| .gitignore | Git 忽略清单 | 不把 runs、缓存等提交进仓库 |
重点说 train.cache。yolov5 在第一次训练时会扫描你配置的图片路径和对应标签,并生成.cache文件,后面每次启动都通过它快速加载数据,不用重新扫一遍。这个文件看起来不起眼,但它是训练环节里报错率最高的文件之一,后面避坑章节会专门讲。
2.3 第一次推理:用预训练权重把检测器跑起来
在动训练之前,先跑一次推理确认环境没问题。即使没有训练好的模型,也可以用官方 COCO 预训练权重试跑:
python detect.py --weights yolov5s.pt --source data/images --conf-thres 0.25 --iou-thres 0.45 --device 0解释一下参数:--weights指定模型权重文件,yolov5s.pt是官方 smallest 版本,首次运行会自动下载;--source可以传图片路径、视频路径甚至摄像头设备号,这里指向项目自带的示例图;--conf-thres是置信度阈值,低于 0.25 的检测框会被丢弃,这个值越大误检越少但漏检越多;--iou-thres是 NMS 的 IoU 阈值,控制重叠框的合并力度。--device 0表示用第一张 GPU,如果没有 GPU 就改成--device cpu。
跑完去runs/detect目录下看结果图。能看到检测框和类别标签,说明环境装好了、推理链路通了。这一步的意义是先把「能不能跑」和「跑得对不对」分开:环境问题在推理阶段就能暴露,不用等训练跑一半才报错。如果你打算用自己的图片测试,把--source指向一个文件夹即可,检测器会自动遍历目录下所有支持的图片格式。
3. TT100K 数据准备:从 JSON 标注到 yolov5 能吃的格式
3.1 TT100K 标注格式与 yolov5 的差异
TT100K 数据集的官方标注是 JSON 格式,每个图片对应一个对象,里面包含若干个objects,每个 object 里有category字段和bbox字段。bbox 给出的是绝对像素坐标的 xmin、ymin、xmax、ymax。而 yolov5 需要的标签是纯文本格式,每行一个目标,五个数字依次是类别编号、归一化后的中心点 x、中心点 y、宽度、高度。
这两个格式之间隔着三层差异:第一,JSON 变 txt;第二,绝对像素坐标变 0~1 归一化坐标;第三,类别名称变类别编号。yolov5 自己带的voc_label.py只能处理 VOC 数据集,对 TT100K 不适用,需要自己写转换脚本。这也是这个资源里最值钱的部分——很多人卡在数据格式转换上,花的时间比训练还多。
类别映射尤其要注意。TT100K 官方标注包含 221 个类别,但绝大多数项目不会用全量训练,因为很多类别样本极少,训练出来也学不好。常见做法是选出现频率最高的几十个类别,或者按你毕设课题需求挑一部分。无论用多少类,都要生成一个固定的类别名单,转换时按名单把字符串类别映射成数字编号。
3.2 转换脚本:JSON 转 YOLO 格式的具体实现
下面这个脚本是我按 TT100K 官方 JSON 结构写的转换逻辑,可以直接改路径使用。核心思路是遍历 annotation.json 里的每个图片,取标注框,做坐标换算,再写到对应 txt 里。
import json import os from pathlib import Path def convert_tt100k(json_path, image_root, label_root, class_names): os.makedirs(label_root, exist_ok=True) class_to_id = {name: idx for idx, name in enumerate(class_names)} with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) for img_name, ann in data['imgs'].items(): img_h = ann['size']['height'] img_w = ann['size']['width'] if img_h == 0 or img_w == 0: continue txt_path = Path(label_root) / (Path(img_name).stem + '.txt') lines = [] for obj in ann['objects']: cat = obj['category'] if cat not in class_to_id: continue bbox = obj['bbox'] xmin = float(bbox['xmin']) ymin = float(bbox['ymin']) xmax = float(bbox['xmax']) ymax = float(bbox['ymax']) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) width = min(max(width, 0.0), 1.0) height = min(max(height, 0.0), 1.0) lines.append(f"{class_to_id[cat]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") if lines: txt_path.write_text('\n'.join(lines), encoding='utf-8') print(f"已转换 {img_name}: {len(lines)} 个目标") if __name__ == '__main__': class_names = ['i5', 'p10', 'p30', 'pl50', 'i4'] # 示例类别,按实际替换 convert_tt100k( json_path='TT100K/annotations/annotation.json', image_root='TT100K/data', label_root='TT100K/labels', class_names=class_names )这段代码的关键点有三个。第一,坐标归一化必须在除以图片宽高之前算出中心点和宽高,顺序反了结果就错了。第二,clip 操作很重要——TT100K 里有些标注框略微越界,不处理的话训练时 yolov5 会报警告甚至直接忽略标签。第三,类别不在你的名单里时直接跳过,这比报错退出更实用,因为你大概率只选部分类别训练。
跑完脚本后,检查一下 labels 目录下 txt 文件的数量和格式,随机打开几个看一眼。我一般会用下面这段命令快速校验标签和图片是否一一对应:
find labels/train -name "*.txt" | wc -l find images/train -name "*.jpg" | wc -l两个数量应该一致,如果差很多,多半是某些图片没有标注框,或者转换脚本里图片名匹配逻辑有问题。
3.3 数据集划分与 data.yaml 配置
TT100K 官方把数据分成 train 和 test 两个集合,但注意它的 test 集是带标注的,很多人直接拿 test 当验证集用。严格来说应该从 train 里再切一部分做验证,这样才能真正反映泛化能力。下面是我常用的划分方式:
mkdir -p tt100k/images/train tt100k/images/val tt100k/labels/train tt100k/labels/val # 从 train 图片中随机抽 15% 作为验证集 ls tt100k/images/train | shuf -n 915 | while read f; do mv tt100k/images/train/$f tt100k/images/val/$f mv tt100k/labels/train/${f%.jpg}.txt tt100k/labels/val/${f%.jpg}.txt done注意shuf是随机打乱,每次执行结果不一样,所以想复现实验结果的话,建议先用shuf -n 915 --random-source=<(seq 100)这种方式固定随机源,或者直接记录一下哪些图片被分到了验证集,免得后面训练结果对不上。
接下来是 yolov5 的数据配置文件,新建一个tt100k.yaml:
train: /绝对路径/tt100k/images/train val: /绝对路径/tt100k/images/val nc: 5 names: ['i5', 'p10', 'p30', 'pl50', 'i4']nc必须和你转换脚本里的class_names长度一致,names的顺序也必须一致,否则训练出来的类别标签就全乱了。这里的names顺序决定了模型的输出索引,和转换脚本里的class_to_id映射是绑定的,改一处就要同步改另一处。
4. 训练与评估:命令、超参与日志判读
4.1 训练启动与关键超参
数据准备好之后就可以开始训练了。我建议用预训练权重做迁移学习,而不是从零训练——yolov5 的骨干网络已经在 COCO 上学到了通用的特征,迁移到交通标志识别上不仅收敛快,最终精度也更高。
python train.py --data tt100k.yaml --weights yolov5s.pt --epochs 100 --batch-size 32 --imgsz 640 --device 0常用参数说明:
| 参数 | 作用 | 建议 |
|---|---|---|
--weights | 预训练权重路径 | 默认yolov5s.pt,不要用''从零训练 |
--epochs | 训练轮数 | 100 起步,看曲线再决定是否加 |
--batch-size | 每批样本数 | 显存不够就减半,同时看学习率是否要调 |
--imgsz | 输入图片尺寸 | 默认 640,小目标多用 960/1280 |
--device | 训练设备 | 0是 GPU,cpu是 CPU |
--patience | 早停耐心值 | 50 表示验证指标 50 轮不涨就停 |
--hyp | 超参文件路径 | 默认data/hyps/hyp.scratch-low.yaml |
这里有个细节:yolov5 训练时会自动检测你数据集的类别数,如果和预训练权重的 80 类不同,它会自动调整输出层。所以你不需要手动改网络结构,但要注意训练日志里会打印Model Summary,确认输出的类别数和你 yaml 里配置的一致。
关于 batch-size,有个经验值:显存 8G 用--imgsz 640时 batch-size 建议 16 到 32;显存不够优先降 batch-size,不要动 imgsz,因为 imgsz 直接影响小目标检测效果,后面避坑章节会细说。
4.2 训练日志与指标怎么读
训练开始后,终端会打印每轮的结果,包含 P、R、mAP50、mAP50-95 和三类 loss。很多新手只看 mAP50,这是不对的。mAP50 只反映框和类别在宽松 IoU 阈值下的匹配率,mAP50-95 才是更严格的指标,它从 0.5 到 0.95 取十个阈值平均,能综合反映框的定位精度。
loss 有三个部分:box_loss 是定位损失,obj_loss 是置信度损失,cls_loss 是分类损失。这三个 loss 在训练前期快速下降是正常的,如果后期出现验证集 loss 反弹上升,而训练集 loss 还在下降,就是过拟合信号,这时候应该提前停或加数据增强。
每个 epoch 结束,runs/train/exp目录下会生成results.png,这个图把 loss 曲线和 mAP 曲线都画在一起了。我判断训练是否结束的方法是看 mAP50-95 曲线:连续 30 轮增长幅度小于 0.5%,就可以停了,再加轮数纯粹浪费时间。
4.3 模型评估与导出
训练结束后,先用验证集做个正式评估,别只看训练日志:
python val.py --data tt100k.yaml --weights runs/train/exp/weights/best.pt --imgsz 640best.pt是验证集上 mAP 最高的权重,last.pt是最后一轮的权重。评估完成后,把模型导出成 ONNX,方便后续部署:
python export.py --weights runs/train/exp/weights/best.pt --include onnx --imgsz 640导出后会生成.onnx文件,这个格式可以用 OpenCV 的dnn模块直接加载推理,不依赖 pytorch 环境,部署的时候非常方便。导出的 ONNX 只是权重和网络结构,输入输出张量格式是固定的,所以导出前想清楚推理时用什么输入尺寸,导出来再改尺寸就要重新导了。
5. 避坑指南:TT100K 训练与推理常见的六个坑
5.1 train.cache 加载失败或路径报错
现象:训练启动时提示Cache not found,或者报train.cache文件损坏、无法读取。
原因:train.cache 是首次扫描数据集时生成的索引,如果你移动了图片或标签的路径,旧的 cache 文件还在,yolov5 读取时发现路径不存在就会报错。
解决:删掉tt100k/images/train和tt100k/images/val目录下的.cache文件再重新训练。也可以给训练命令加参数禁用 cache:
python train.py --data tt100k.yaml --weights yolov5s.pt --no-cache从那以后我每次做数据增删都会先删 cache 再训练,省得莫名其妙地报错。
5.2 类别数不一致导致训练崩溃或指标异常
现象:训练日志显示nc为 80,但你的数据只有 5 类,或者模型训练完预测出来的全是同一个类别。
原因:当--weights指定了预训练权重时,yolov5 会检测权重里的类别数。如果数据 yaml 的nc和权重不匹配,有时它能自动调整,有时会静默地用错误的输出层,尤其当你用了旧版本或修改过模型文件时。
解决:训练前确认数据 yaml 的nc和names,修改模型配置时用官方提供的参数,不要手动改 yaml 模型结构文件。转换脚本里用class_to_id映射时,检查生成 txt 的类别编号最大值是否小于nc。常见做法是写一行快速校验命令:
awk '{print $1}' tt100k/labels/train/*.txt | sort -n | uniq -c看到的最大编号必须小于等于nc,否则你的类别映射写错了。
5.3 小目标漏检严重,mAP50 高但 mAP50-95 很低
现象:模型在验证集上 mAP50 有 0.8 以上,但 mAP50-95 只有 0.3,预测时远处的交通标志总是检测不到。
原因:TT100K 里的交通标志在整图中占比很小,尤其远距离标志只有十几像素。yolov5 默认的 640 输入尺寸会把小目标压缩得更小,导致特征不明显。
解决:把--imgsz提到 960 或 1280,代价是显存占用和训练时间增加。我实测过,同一个数据集 640 提到 960,mAP50-95 能涨 8 到 12 个点。如果显存不够,可以改用--imgsz 1280加批次 8,再加上--cache把图片预加载到内存加速。
5.4 显存不足,训练直接 OOM 崩溃
现象:启动训练后几秒内报CUDA out of memory。
原因:batch-size 或 imgsz 太大,占满了显存。很多人把所有参数设成教程里的默认值,但默认值是按 24G 显存配的。
解决:把--batch-size降到 8 或 4,--imgsz降到 640,确认能跑通后再逐步加大。yolov5 默认会自动调 batch-size,但手动指定更可控。顺便说一句,如果你开了--cache,内存不足也可能报这个错,关掉--cache试试。
5.5 标签归一化越界或为空
现象:训练时打印WARNING: image ... labels shape ...,或某张图完全没有标签参与训练。
原因:转换脚本中某些异常框导致宽高或中心点超出 [0,1] 范围,被 yolov5 过滤掉了。
解决:转换时做 clip 已经把越界框压回边界,但如果出现负数或者宽高为零,要检查原始 JSON 数据。TT100K 有些标注框尺寸为零或为负,直接把这几个目标跳过就行,别让它们拖垮整个训练。
5.6 CPU 训练太慢,一天跑不了多少轮
现象:--device cpu训练时一个 epoch 要跑三四十分钟,完全没有耐心等。
原因:yolov5 默认按 GPU 显存自动调整 worker 数和批大小,CPU 上这些设置不生效,训练速度完全靠单核算力。
解决:用--device cpu时加上--workers 0试试,有些环境多进程反而拖慢速度;或者放弃训练,直接用预训练权重做特征提取和推理。如果没有 GPU,其实更适合用这个项目的推理链路做检测,训练可以放到云上 GPU 服务器跑,花几十块钱比本地难受一整天划算。
6. 把模型用起来:推理阈值、NMS 后处理与结果导出
模型训练好之后,真正落地是推理这一步。detect.py 里有两个参数决定检测效果:--conf-thres和--iou-thres。conf-thres是置信度门限,交通标志场景下我一般设 0.2 到 0.3,因为标志小、特征弱,调太高容易漏检;iou-thres控制 NMS 合并,默认 0.45 够用,但如果你在同一区域同时检测到多个重叠标志,可以降到 0.3 减少误合并。
python detect.py --weights runs/train/exp/weights/best.pt --source test_images --conf-thres 0.25 --iou-thres 0.45 --imgsz 960 --save-txt --save-conf--save-txt会把检测结果保存成 txt,每行格式是类别编号 x1 y1 x2 y2 置信度,方便后续做数据分析或计算指标;--save-conf把置信度一并写入。--project和--name可以改输出目录,比如--name exp_test避免覆盖之前的推理结果。
大图推理有一个实用技巧:TT100K 原图分辨率高,直接把整图塞进模型效果不好,常见做法是先用普通的前景分割或滑动窗口截出小图,再对小图做检测。如果不想写滑动窗口,也可以把--imgsz设高再用测试集跑一遍对比 mAP。我自己在测一个路侧场景时,就是用 1280 输入把之前漏掉的小标志全找回来的。
这套东西我前后改了三次数据划分才把验证集做稳,第一次连 labels 映射写错都没发现,模型训练出来全是同一个类别。从那以后我每次拿到新数据集,都强制先跑完转换脚本和标签校验,再进训练流程。这次把整个过程整理出来,也是给自己留个可复现的记录,希望帮到你。
本文还有配套的精品资源,点击获取