☰
YOLOv5+TT100K交通标志识别全流程指南:从数据转换到部署
2026/10/1 18:42:08 网站建设 项目流程

简介:一套基于YOLOv5的交通标志牌识别完整项目,主要面向计算机、人工智能、电子信息等相关专业的学生,也适合算法初学者作为实战练习。项目选用TT100K数据集进行模型训练与验证,代码经过测试可稳定运行,可直接用于课程大作业、毕业设计或初期项目演示。资源包共147个文件,结构上以56个Python源码文件为核心,覆盖数据集配置、模型训练、检测推理等环节;同时包含YAML/YML参数配置、Shell辅助脚本、Dockerfile多环境部署配置、Markdown说明文档以及Jupyter Notebook示例,方便快速复现和二次开发,压缩包仅1.1MB,便于下载和迁移。目前已有352人学习下载,适合对照YOLOv5工程化流程深入理解数据配置、训练调参与部署思路,是从理论走向项目实践的高性价比参考资料。整体代码注释清楚,目录组织规范,可帮助读者快速定位并复用各个功能模块。

1. 交通标志牌识别项目解压后从哪里入手:yolov5、tt100k与python源码的完整链路

拿到一份《基于yolov5的交通标志牌识别项目(python源码+项目说明)(使用tt100k数据集).zip》,很多人第一件事是解压、找README、然后直接跑训练脚本。真实情况是:代码能跑通,但换一台机器、换一批数据就翻车,原因基本都藏在数据准备上。tt100k的标注格式不是yolov5默认能吃的,类别体系又细又杂,直接拿来训练,mAP会低到怀疑人生。这篇笔记以这套源码包为主线,把三件事讲透:tt100k怎么转成yolov5的标注、训练参数怎么调才不掉坑、模型导出后如何在边缘设备上落地。适合课程设计、毕业论文,以及想把标志识别装进路口摄像头或车载盒子的工程师。下面从数据集开始拆。

2. tt100k数据怎么喂给yolov5:json转txt标注、类别映射与筛选脚本

yolov5训练需要的数据格式很简单:每张图对应一个同名txt,每一行是class x_center y_center width height,坐标全部归一化到0~1。而tt100k官方给的是json标注,里面按图片名挂着一串对象框,所以第一步永远是转换格式。这一步做不好,后面训练、推理全都会错位。

2.1 tt100k的标注结构与类别体系

tt100k是清华大学基于腾讯街景构建的交通标志数据集,总量超过10万张街景图像,其中大约三分之一的图像带精细标注。它的标注体系分大类和小类,例如禁令、警告、指示、其他,下面再细分出数字、符号、颜色等差异。官方标注json的结构大致如下:

{ "imgs": { "0.jpg": { "path": "train/0.jpg", "objects": [ {"category": "i200", "bbox": {"xmin": 112, "ymin": 245, "xmax": 140, "ymax": 273}} ] } } }

这个结构里,imgs是图片名到标注信息的映射,objects里每个元素代表一个交通标志,category是类别字符串ID,bbox是像素坐标的左上角和右下角。yolov5要的是归一化中心点加宽高,所以转换脚本要做的就是把像素坐标算成归一化值。这里有一个容易踩的坑:部分json里的bbox字段名可能是x、y、w、h,也可能是xmin、ymin、xmax、ymax,拿到数据后先打印一条样本看清楚再写脚本。另外,tt100k超过200个小类,如果全部保留,很多类别只有几十上百个样本,训练出来的权重就是个摆设。所以类别映射和筛选必须在转换阶段完成,而不是等训练完发现mAP不对再返工。

2.2 把TT100K的json标注转成YOLO txt:转换脚本与参数说明

下面这份脚本是我处理这类数据集时常用的方案,直接照着改就能用:

# convert_tt100k.py # 将TT100K的json标注转换为YOLOv5需要的txt格式 import json import os import cv2 from pathlib import Path # class_map: 原始category -> 训练用类别ID # 这里把多种限速标志合并成speed_limit,多种禁止标志合并成prohibition class_map = { "i200": 0, "i210": 0, "i220": 0, # 限速类 "p100": 1, "p110": 1, "p120": 1, # 禁止类 "w100": 2, "w110": 2, "w120": 2, # 警告类 } def convert(json_path, img_root, out_dir): Path(out_dir).mkdir(parents=True, exist_ok=True) with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) for img_name, info in data['imgs'].items(): # 读图像拿宽高,比json里的字段更保险 img_path = os.path.join(img_root, img_name) if not os.path.exists(img_path): continue h, w = cv2.imread(img_path).shape[:2] lines = [] for obj in info.get('objects', []): cat = obj['category'] if cat not in class_map: continue b = obj['bbox'] # 防止坐标出界导致训练时loss为nan xmin = max(b['xmin'], 0) ymin = max(b['ymin'], 0) xmax = min(b['xmax'], w - 1) ymax = min(b['ymax'], h - 1) if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h lines.append( f"{class_map[cat]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}" ) if lines: out_file = Path(out_dir) / (Path(img_name).stem + '.txt') out_file.write_text('\n'.join(lines), encoding='utf-8')

逻辑说明:脚本逐张读取json里的imgs,对每张图先用cv2.imread拿真实宽高,而不是信任json里的宽高字段。然后遍历每个objects,用class_map判断要不要保留、保留成哪个新ID。bbox先做边界裁剪再归一化,防止坐标出界导致loss变成nan。最后只把有标注的图写出txt,没有objects的图不生成txt,这些图在yolov5里会自然当作负样本参与训练。参数说明:class_map是核心,新ID必须与后面tt100k.yaml里names的顺序一一对应,否则训练出来的模型类别标签全错;out_dir建议分别设成labels/train和labels/val;img_root指向原图根目录。转换完成后,最好抽查几个txt,确认坐标值都在0~1之间,并且类别ID没有跳号。

2.3 类别筛选:不要一上来就训200类

tt100k官方类别里,很多小类只有几十个样本,直接训练结果是:低频类mAP几乎为0,显存占用高,训练时间拉长,调试难度翻倍。我一般会先用一段统计脚本看看各类别样本数,再决定映射表:

from collections import Counter cnt = Counter() for info in data['imgs'].values(): for obj in info.get('objects', []): cnt[obj['category']] += 1 print(cnt.most_common(30))

逻辑说明:这段统计每个原始类别ID在所有标注中出现的次数,most_common(30)直接列出样本最多的30个类别。参数说明:如果要保留更多类别,把30换成100,但建议先跑一遍看清楚分布再决定。做课程设计或演示的话,把类别控制在10个以内,模型mAP会明显更好看;如果是真实的道路标志识别项目,建议按大类合并,比如把所有限速牌合并成speed_limit,所有禁止标志合并成prohibition,把类别数压到20~40个。交通标志识别里有一个“玄学”:类别越少,mAP越好做,演示效果越稳。不要追求类别齐全,先把识别准确率做上去,再考虑扩展类别。

3. yolov5环境配置与数据划分:conda依赖、项目目录和tt100k.yaml

数据转好之后,很多人卡在“yolov5环境配置”这一步:python版本和torch版本不匹配、conda装不上、显卡驱动和CUDA对不上。这里给一套比较稳的方案,按顺序执行基本不会翻车。

3.1 用conda建环境:一份能复现的依赖清单

# 创建python 3.9环境,避免python 3.11与某些算子不兼容 conda create -n tt100k python=3.9 -y conda activate tt100k # 先装pytorch,再装yolov5依赖,顺序不要反 pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 从GitHub官方仓库clone一份yolov5源码 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt

逻辑说明:先装pytorch再装requirements,因为requirements里很多包在安装时会探测torch版本,顺序反了容易装错依赖。cu118对应CUDA 11.8运行时,绝大多数RTX系列显卡都能直接用。参数说明:python 3.9是目前yolov5兼容性最稳的选择之一,不要为了追新上3.12;如果只有CPU机器,把cu118换成cpu,训练会慢很多但能跑;torch 2.0.1不是越老越好,但足够yolov5使用。装完跑一句python train.py --data coco128.yaml --epochs 1验证环境,能正常启动说明环境没问题。

3.2 项目目录怎么摆:让你少改十处路径

常见的项目目录结构是这样的,这个结构能最大程度减少路径改动:

tt100k_yolov5/ ├── yolov5/ # 官方源码,train.py和detect.py都在这里 ├── datasets/ │ └── tt100k/ │ ├── images/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ └── labels/ │ ├── train/ │ └── val/ └── runs/ # 训练输出的权重和日志

yolov5的data配置支持相对路径,只要images和labels的子目录名字对应上,比如images/train对labels/train,就能少踩很多路径坑。项目说明里如果给了源码包,不要强行改源码里的绝对路径,尽量用yaml的相对路径配置。数据集和源码平级放,这样换机器时只需要改yaml里的path字段。接着创建datasets/tt100k.yaml:

# datasets/tt100k.yaml # path是相对yolov5代码目录的路径,不要写成绝对路径 path: ../datasets/tt100k train: images/train val: images/val test: images/test nc: 3 names: 0: speed_limit 1: prohibition 2: warning

参数说明:path是相对于yolov5代码目录的路径,写成../datasets/tt100k可以保证在code目录下用相对路径启动训练;train和val是相对path的路径,不要再加datasets/tt100k前缀;nc必须等于类别数3,如果class_map里最大ID是2,nc就是3;names的顺序必须和class_map的ID一一对应。这个yaml写错是训练时报错最多的地方,报错信息通常是AssertionError: train: No labels in ...,一旦看到这个,优先检查路径。

3.3 数据划分:随机划分还是按路段划分

# split_data.py # 按文件名随机划分15%做验证集 import random from pathlib import Path random.seed(42) images = list(Path('datasets/tt100k/images/all').glob('*.jpg')) random.shuffle(images) val_cnt = int(len(images) * 0.15) val_set = set(images[:val_cnt]) for img in images: dest = 'val' if img in val_set else 'train' img.rename(img.parent.parent / 'images' / dest / img.name) label = img.with_suffix('.txt') if label.exists(): label.rename(label.parent.parent / 'labels' / dest / label.name)

逻辑说明:先把所有jpg打乱,取前15%作为验证集,其余作为训练集。然后将图片移动到对应目录,同时移动同名txt文件。参数说明:random.seed(42)保证每次划分结果一致,方便复现;15%是常见划分比例,数据量小可提高到20%。这个脚本的局限是纯随机划分,而tt100k很多图片来自同一路段的连续街景帧,随机切分会造成验证集里出现训练集同场景的画面,mAP虚高。更严格的做法是按json里的场景ID划分,保证同一路段只出现在一个集合里。如果只是课程设计,随机划分也够用;如果做真实项目,建议花时间做场景级划分。

提示:划分完成后,检查val目录下jpg和txt一一对应,缺txt的图要移出val,否则训练yolov5会报找不到标签。

4. 训练与超参数调优:输入尺寸、数据增强与类别不平衡

数据准备好,进入训练阶段。新手最常见的错误是拿yolov5默认参数直接训,那些参数是为COCO大目标设计的,tt100k这种街景小目标场景需要针对性调整。这里按影响程度从大到小讲几个关键点。

4.1 基线训练命令与日志怎么看

# 在yolov5目录下执行 python train.py --data ../datasets/tt100k.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 80 \ --device 0 \ --name tt100k_base

参数说明:--weights yolov5s.pt用COCO预训练权重做迁移学习,比从头训练收敛快很多,也稳定很多;--img 640是输入图片尺寸,对大部分路口的标志牌够用,但远处的限速牌可能只有二三十像素,后面要单独实验;--batch 16在8G显存以下的显卡上容易OOM,可以降到8,显存不够时优先降batch而不是降imgsz;--epochs 80对tt100k这种数据量足够,如果验证集mAP还在涨就继续加。日志主要看三个数:mAP@0.5、mAP@0.5:0.95和val/obj_loss。前20轮mAP不涨是正常的,迁移学习刚开始在适应新数据分布;如果80轮后mAP@0.5还不到0.5,基本是数据转换或划分有问题,回头查第2章。

4.2 对交通标志牌影响最大的几个超参数

参数默认值建议值原因
imgsz640640或1280标志牌小目标多,高分辨率能显著涨点
batch16按显存最大设置batch过小会让batch norm统计不稳定
lr00.010.01~0.02学习率过大容易loss变nan
close_mosaic1010~20最后n个epoch关闭mosaic,让模型适应真实分布
anchor自动不手动改yolov5会在训练集上重新聚类anchor

imgsz从640调到1280,小目标召回率通常能提升5到10个点,代价是显存翻倍、训练时间翻倍。close_mosaic是yolov5官方后期加入的trick:mosaic增强会把四张图拼在一起,导致小目标被缩放得面目全非,最后若干epoch关掉它让模型在真实分布上微调。如果你拿到的源码版本比较老,没有这个参数,可以手动在最后15个epoch把mosaic概率置为0。lr0对tt100k这种数据量不算敏感,但配合--cos-lr调度器能更稳。另外,如果显卡显存只有6G,建议直接换yolov5n或yolov5s,不要硬上yolov5l,训练时长和显存占用都会让你怀疑人生。

4.3 类别不平衡与小目标的处理

tt100k的类别样本量差异极大,多的几千张,少的几十张。处理办法有两个,第一个是第2章说的在数据转换时筛掉低频类别,这是最省事的办法;第二个是对保留的少样本类别做过采样,让模型在一个epoch里多看到几次这类样本。过采样的做法是复制包含稀有类别的图片和标注文件:

# oversample.py from collections import Counter import shutil from pathlib import Path labels_dir = Path('datasets/tt100k/labels/train') img_dir = Path('datasets/tt100k/images/train') for txt in labels_dir.glob('*.txt'): # 读取该图包含的所有类别ID classes = {int(line.split()[0]) for line in txt.read_text().strip().splitlines() if line} # 假如类别3是稀有类,复制3份 if 3 in classes: for i in range(3): shutil.copy(txt, labels_dir / f"{txt.stem}_dup{i}.txt") shutil.copy(img_dir / f"{txt.stem}.jpg", img_dir / f"{txt.stem}_dup{i}.jpg")

逻辑说明:脚本读取每张图的txt,提取里面的类别ID集合,如果包含目标稀有类,就把图片和标注复制三份。复制后的新文件名不能与原文件重名,所以加了_dup0、_dup1、_dup2后缀。参数说明:复制倍数一般2到4倍足够,太多会过拟合;yolov5的mosaic增强会把多张图拼在一起,所以只复制图片不复制标注的做法不推荐,复制标注文件更安全。小目标问题除了提imgsz,还可以在模型层面启用P2层输出,但改动成本高,我一般先试imgsz和过采样,效果不够再考虑改模型结构。如果训练时发现某个类别mAP一直为0,先看它的样本数,少于100的类别在tt100k里基本训练不出来。

4.4 早停与best.pt的选择

yolov5默认会在runs/train/tt100k_base/里保存best.pt和last.pt。best.pt是验证集mAP最高的权重,last.pt是最后一个epoch的权重。很多项目说明会让直接用best.pt,但这里有个坑:如果验证集划分不合理,best.pt可能是过拟合到验证集的结果。我的习惯是训练结束后用test集重新测一遍best.pt,如果test mAP比val mAP低很多,说明划分出了问题,回头检查第3.3节。选权重时,部署用best.pt,断点续训用last.pt。另外,yolov5的早停参数patience默认是100个epoch,如果你的数据量小,建议把它改成20,节省时间的同时也能避免过拟合。

5. 推理与部署避坑:后处理参数、实景漏检与五个常见问题

训练结束,项目说明里一般会教你用detect.py跑推理。但实际上真正影响项目成败的是后处理参数,以及那些只在实景里才会暴露的问题。这一章把推理跑通,再把高频坑一个个拆开。

5.1 用detect.py跑单张图和视频:参数怎么设

# 在yolov5目录下执行 python detect.py --weights runs/train/tt100k_base/weights/best.pt \ --source test.jpg \ --img 640 \ --conf 0.5 \ --iou 0.45 \ --save-txt

参数说明:--conf 0.5是置信度阈值,低于这个值的框会被丢掉,阈值越高误检越少但召回也越低;--iou 0.45是NMS的IoU阈值,交通标志之间很少重叠,用0.45到0.55都行;--save-txt会把检测结果保存成txt,方便后面算mAP。如果--source填一个视频文件或摄像头设备号,模型会逐帧推理,但帧率可能只有几fps,这是正常现象。先跑单张图确认推理正常,再上视频流,这样能避免把问题混在一起排错。

5.2 yolov5后处理:NMS与置信度到底改哪个

yolov5的推理结果不是直接从网络输出画框,网络输出的是大量预测框,必须经过后处理才能变成最终结果。后处理三件套是:conf_thres过滤低分框,iou_thres做NMS去重,max_det限制最大输出框数。实景路口最常见的误检来源是小广告牌被当成限速牌,这种问题靠调iou_thres没用,要把conf_thres调到0.6以上,或者在训练时加入负样本。另一个容易被忽略的点是类别置信度:当多个类别外观接近时,模型会输出接近的分数,此时不要只信最高分,要看最高分和次高分之间的差值,差值过小说明模型不确定,宁可不输出。后处理参数看起来是小事,但它直接决定实景部署的误报率,调试时要一组一组对比,不要随手改。

5.3 五个高频坑:现象、原因与解决

坑1:训练时loss变成nan或精度一直为0 现象:train/loss输出nan,或者训练了几十个epoch,mAP@0.5始终是0。 原因:最常见的是标签里有0宽度或0高度的框,或者类别ID超出了yaml里nc的个数。 解决:写脚本检查labels目录里每个txt,凡是w或h为0的行直接删掉;检查class ID是否小于nc;再用cv2.imread逐张检查图像,读不出来就跳过。这个坑几乎每个第一次做数据转换的人都会遇到,检查一遍就通。

坑2:验证集mAP很高,但换一批图片检测效果很差 现象:随机划分的val上mAP@0.5到0.8,实景图片一测漏检严重。 原因:数据划分时用了随机切分,同一路段连续帧被同时分进train和val,模型等于见过答案。 解决:按拍摄路段或时间切片划分数据,保证同一路段只出现在一个集合里。课程设计可以偷懒,真实项目一定要做场景级划分。

坑3:检测框位置没问题,但类别标签全错 现象:框框得很准,但框上的类别名全是乱的,比如限速牌识别成了禁止标志。 原因:class_map字典、yaml里names顺序、txt里class ID三者不一致。yolov5不校验类别名,只按数字索引,三处错一处就全错。 解决:写一个验证脚本,检查txt里所有class ID的最大值必须小于yaml里的nc;再打印几行txt,对照names逐行确认。

坑4:训练或推理时显存OOM 现象:RuntimeError: CUDA out of memory。 原因:batch过大,或者imgsz调到1280后没降batch。 解决:batch降到8或4;如果用了--cache预加载数据,显存会进一步上涨,关掉它;还不行就换yolov5n或yolov5s的轻量权重。对交通标志识别来说,yolov5s的精度和速度最平衡。

坑5:图像分辨率不足导致小目标漏检 现象:远处的限速牌检不出来,走近才出框。 原因:原图1920x1080,yolov5缩到640后,远处标志只有十几个像素,特征已经丢失。 解决:提升imgsz到1280,或者用滑窗分块检测:把大图切成多块分别推理再合并结果。分块会带来同一目标被重复检测的问题,合并后用全局NMS处理。

提示:前两条坑影响训练可信度,后三条坑直接影响部署效果。建议按顺序排查,先查数据再调参数,不要一上来就怀疑模型结构。

6. 导出onnx和模型量化:在树莓派上跑通yolov5标志识别

训练好的权重要落地,最常用的办法是先导出onnx,再针对目标平台做推理加速。yolov5官方提供了导出脚本,一条命令就能完成:

python export.py --weights runs/train/tt100k_base/weights/best.pt \ --include onnx \ --opset 12 \ --dynamic

参数说明:--include onnx指定导出onnx格式;--opset 12是ONNX算子集的版本,老版本onnxruntime要低一点,新版本无所谓;--dynamic允许输入尺寸动态变化,方便部署时自由选择imgsz。注意:如果后面要做int8量化,建议去掉--dynamic,因为动态shape会兼容大部分量化工具,但静态shape更稳。导出后用onnxruntime加载模型推理,精度和PyTorch几乎一致,这一步相当于给模型吃了“后悔药”——以后换部署平台不用重新训练。

树莓派上跑yolov5标志识别,我实测过树莓派5的CPU:yolov5s加640输入,单帧推理约1.5到3秒,基本不可实时。常见做法是把模型换成yolov5n,imgsz降到320,用onnxruntime的多线程跑,单帧约300到500毫秒,配合跳帧检测能到10fps左右。如果你的板子有NPU,比如瑞芯微或Jetson系列,int8量化才有明显加速;纯CPU上int8量化收益有限,主要作用是减小内存占用。精度对比上,yolov5s从fp32量化到int8,mAP@0.5通常会掉2到5个点,对交通标志识别来说还能接受。

这个项目做到最后,我最大的教训是:不要为了帧率盲目降imgsz。把320的模型部署到真实路口,结果漏掉了30米外的限速牌,项目差点被打回。后来改成640输入加跳帧检测,才把速度和准确率的账算平。如果你想把这个方案放进自己的项目,建议先定场景:课程设计就用best.pt跑demo,面向真实路口就要在imgsz和后处理上下真功夫。希望帮到你。

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

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

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

立即咨询