
简介基于YOLOv5的交通标志牌识别项目源码以TT100K数据集为训练基础代码已完成运行测试功能正常。资源主要面向计算机视觉相关专业的在校学生、毕业设计团队及企业算法工程师既能作为课程设计、毕业设计或大作业的完整项目参考也适合刚接触YOLO系列目标检测模型的初学者进行实战练习。压缩包共147个文件整体约1.1MB文件构成以Python源码、YAML配置文件、Shell辅助脚本及Jupyter Notebook为主同时包含多份Dockerfile、Markdown项目说明、训练缓存等资源覆盖数据集配置、模型训练、推理验证与环境部署等关键环节。目前资源已有352人学习。通过该项目可以快速搭建YOLOv5交通标志识别实验环境完整复现从数据准备到模型评估的流程项目中的目录结构和说明文档也能帮助读者理解检测模型训练细节并以此为基础扩展识别类别或迁移到其他目标检测场景是一份兼顾学习与实践的工程模板。1. 基于YOLOv5的交通标志牌识别用TT100K跑通一个能交差的检测项目手头这份资源不是花架子是把 YOLOv5 在 TT100K 数据集上从数据准备、模型训练到推理验证的完整工程。下载下来就能跑里面带的 Python 源码、项目说明、配置文件和 Dockerfile是照着课程设计或毕业设计的要求整理的。适合两类人一是课程大作业、毕设需要“能演示、有数据、能调参”的目标检测项目的人二是想弄懂 YOLOv5 实战流程但不想从零搓数据集的从业者。先说一个反直觉结论TT100K 有 221 类标志直接拿官方预训练权重去测精度惨不忍睹必须做类别过滤和格式转换。这份资源的核心价值就是替你把这些坑走了一遍。2. 数据准备与格式转换TT100K 到 YOLOv5 的五个关键动作2.1 为什么要自己转换 TT100K 标注格式TT100K 是腾讯与清华发布的交通标志数据集包含 10 万张街景图像官方标注以 XML 文件形式存放在annotations目录中。而 YOLOv5 要求每张图片对应一个同名的.txt标签文件每行格式为class x_center y_center width height坐标值必须归一化到 0~1 之间。直接拿官方的 XML 去训练YOLOv5 的 dataloader 根本读不了。常见做法是写一个 Python 脚本把 XML 里的object节点解析出来提取每个目标的类别名称和边界框坐标。这里必须小心一个细节TT100K 的标注有两种框box是原始框points是多边形顶点。YOLOv5 只需要矩形框所以只取box字段即可。部分样本的box属性可能缺失脚本里要做一次 None 判断否则程序会中断。另外TT100K 原始类别有 221 种但很多类别样本数极少。实际工程里一般做类别映射把 221 类归并成 3~5 个大类比如“禁止标志(prohibitory)”“警告标志(warning)”“指示标志(mandatory)”等。这样既降低训练难度也缓解样本不平衡。映射关系建议做成一个 Python 字典输出类别时查表转换。2.2 转换脚本的完整实现与参数说明把下面的脚本存为convert_tt100k.py和你的annotations目录放在同一级。它会遍历所有 XML生成 YOLOv5 格式的标签文件并同步生成train.txt和val.txt路径清单。import os import xml.etree.ElementTree as ET from tqdm import tqdm # 类别映射只保留三大类其他统一归为 unknown(不参与训练) CATEGORY_MAP { p: prohibitory, # 禁止标志 w: warning, # 警告标志 i: mandatory, # 指示标志 po: prohibitory, # 部分标注重叠做二次映射 pm: mandatory, pl: prohibitory } def convert_annotation(xml_path, out_dir, img_width2048, img_height2048): 解析单个TT100K XML标注输出YOLOv5格式的txt标签文件 if not os.path.exists(xml_path): return None tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): category obj.find(name).text.lower() if category not in CATEGORY_MAP: continue # 跳过未映射的类别 cls_id CATEGORY_MAP[category] box obj.find(box) if box is None: continue # 没有矩形框的样本跳过 xmin float(box.get(xmin)) ymin float(box.get(ymin)) xmax float(box.get(xmax)) ymax float(box.get(ymax)) # 归一化到0-1 center_x (xmin xmax) / 2.0 / img_width center_y (ymin ymax) / 2.0 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height lines.append(f0 {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}\n) if not lines: return None # 输出文件名与图片名一致只是扩展名改为txt out_name os.path.basename(xml_path).replace(.xml, .txt) out_path os.path.join(out_dir, out_name) with open(out_path, w) as f: f.writelines(lines) return out_path def main(ann_dir, label_dir, img_dir, train_txt, val_txt): os.makedirs(label_dir, exist_okTrue) xml_files [f for f in os.listdir(ann_dir) if f.endswith(.xml)] train_imgs, val_imgs [], [] # TT100K 官方划分train/val 目录已分开这里直接按目录取 for xml_file in tqdm(xml_files): xml_path os.path.join(ann_dir, xml_file) # 假设所有XML都在同一个目录如果图片在子目录需要自己拼接 img_path os.path.join(img_dir, xml_file.replace(.xml, .jpg)) if not os.path.exists(img_path): continue # 这里随机抽20%做验证集正式使用建议按官方划分 import random if random.random() 0.8: train_imgs.append(img_path) else: val_imgs.append(img_path) convert_annotation(xml_path, label_dir) with open(train_txt, w) as f: f.write(\n.join(train_imgs)) with open(val_txt, w) as f: f.write(\n.join(val_imgs)) print(f转换完成: {len(train_imgs)} 训练图, {len(val_imgs)} 验证图) if __name__ __main__: main( ann_dirannotations, label_dirlabels, img_dirimages, train_txttrain.txt, val_txtval.txt )这个脚本有几点需要说明。第一CATEGORY_MAP里我把p、po、pl都映射到prohibitory是因为 TT100K 的原始标注中对同一类标志用了不同的前缀不合并会让类别数量爆炸而且相互混淆。第二坐标归一化用的是img_width和img_heightTT100K 原始图像是 2048×2048但实际下载的数据集可能已经做了裁剪建议先查看你的图片尺寸把这两个参数改成真实值算错会导致框整体偏移。第三我用了随机抽样切分训练集和验证集只是为了快速演示正式跑实验时请按 TT100K 官方的train/val目录划分否则会出现同一场景的相邻帧一张在训练集、一张在验证集导致指标虚高。2.3 数据校验查看标签是否正常转换完成后别急着训练。先用一段简单代码检查生成的.txt内容是否落在 0~1 区间内以及每张图的标签是否为空。这个步骤能避免训练时出现“no labels”警告。import os label_dir labels empty_cnt 0 bad_cnt 0 for name in os.listdir(label_dir): path os.path.join(label_dir, name) with open(path) as f: lines f.readlines() if len(lines) 0: empty_cnt 1 continue for line in lines: parts line.strip().split() if len(parts) ! 5: bad_cnt 1 break coords [float(x) for x in parts[1:]] if any(c 0 or c 1 for c in coords): bad_cnt 1 break print(f空标签: {empty_cnt}, 异常标签: {bad_cnt})运行时如果bad_cnt很大说明图片尺寸参数设置错了。我习惯在转换脚本里直接读取PIL获得真实宽高而不是写死数字。空标签文件建议直接删除否则 YOLOv5 训练时会在 dataloader 里反复跳过这些图片拖慢速度。3. 依赖环境与训练参数从零跑通 YOLOv5 的训练流程3.1 环境搭建的取舍与版本组合选择YOLOv5 的官方仓库对运行环境的依赖不算苛刻但版本组合有讲究。项目自带的requirements.txt通常会固定torch1.7、opencv-python4.1.2、matplotlib等。我常用的环境搭配是 Python 3.8 加 CUDA 11.3 加 PyTorch 1.12这套组合在 RTX 30 系列显卡上表现稳定编译 nms 等 C 扩展时不会报错。如果只做 CPU 推理可以跳过 CUDA 安装但训练速度会慢到一个不可接受的程度——TT100K 训练集上万张图CPU 上跑 100 个 epoch 基本要一整天。项目里带了多个 Dockerfile比如Dockerfile-arm64和Dockerfile-cpu。用 Docker 的好处是省去装环境的麻烦但如果你在 Windows 本机上做课程演示我更推荐用 Anaconda 新建一个虚拟环境避免破坏系统 Python。创建命令如下conda create -n yolo python3.8 -y conda activate yolo pip install -r requirements.txt pip install torch1.12.0cu113 torchvision0.13.0cu113 --extra-index-url https://download.pytorch.org/whl/cu113pip install torch这行带--extra-index-url原因是 PyTorch 官方 CUDA 版本的 wheel 放在独立索引里直接从默认 PyPI 安装会拿到 CPU 版。如果网速慢可以改成在本地下载好 wheel 再装。装完验证一下import torch print(torch.__version__) print(torch.cuda.is_available())输出True说明 CUDA 可用可以直接进入下一步。3.2 数据配置文件与训练超参数的设置逻辑YOLOv5 要求用 YAML 文件描述数据集。在项目里创建traffic.yaml内容如下train: train.txt val: val.txt nc: 1 names: [traffic_sign]注意nc是类别总数。如果你按上一章做了三分类映射这里就改成nc: 3names对应改成[prohibitory, warning, mandatory]。train.txt和val.txt的路径建议写绝对路径尤其是用相对路径时容易出现训练中途报错“路径不存在”。这个错误很隐蔽因为 YOLOv5 不会提前校验只在第一个 epoch 加载数据时才报错白白浪费等待时间。训练命令的核心参数如下python train.py \ --data traffic.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0 \ --workers 4 \ --project runs/train \ --name traffic_sign参数含义逐个说。--img 640是输入分辨率TT100K 原始图是 2048×2048直接缩到 640 会丢失小目标信息训练阶段可以先 640后面测试评估时再用 1280 做一次对比。--batch 16受显存限制12GB 显存跑yolov5s用 16 稳妥用 32 会报 CUDA out of memory。--weights yolov5s.pt代表用 COCO 预训练权重做迁移学习这比从头训练收敛快得多。--device 0指定第一张显卡机器只有 GPU 可以忽略。--workers 4是数据加载线程数Windows 上超过 4 偶尔会报 DataLoader worker 崩溃直接改成 2 就安静了。3.3 训练过程中的指标含义与日志观察训练启动后终端会持续输出box_loss、obj_loss、cls_loss和mAP等指标。对新手来说最重要的判断标准是验证集上的mAP0.5它超过 0.7 说明模型基本可用。box_loss下降缓慢不代表训练错了YOLO 的 loss 本身是多任务加权和只要数值整体下降、没有剧烈震荡就正常。训练结束后模型权重保存在runs/train/traffic_sign/weights/best.pt和last.pt。best.pt是验证集 mAP 最高的权重last.pt是最后一轮的。课程演示和后续推理都用best.pt。如果训练结果不理想比如 mAP 一直低于 0.5优先检查标签文件是否为空、类别 ID 是否从 0 开始。YOLOv5 的类别 ID 是 0 基索引而 TT100K 的原始类别字符串没有数字转换时用字典做维护这一点踩的人最多。4. 模型评估与推理验证不只是跑通还要能说清效果4.1 用 test.py 和 detect.py 分别做评估与实景推理项目里自带的test.py用于在验证集上计算 mAP、召回率、精确率。命令如下python test.py \ --weights runs/train/traffic_sign/weights/best.pt \ --data traffic.yaml \ --img 640 \ --batch 16 \ --device 0运行完能看到每个类别的AP和平均mAP。这一步是课程报告里的核心数据建议把输出表格截图保留。detect.py用于对单张或整个文件夹做推理命令如下python detect.py \ --weights runs/train/traffic_sign/weights/best.pt \ --source test_images \ --img 640 \ --conf-thres 0.4 \ --iou-thres 0.5 \ --save-txt \ --project runs/detect \ --name traffic_demo--conf-thres 0.4是置信度阈值低于这个值的检测框会被丢掉。--iou-thres 0.5是 NMS 的 IoU 阈值控制重叠框的合并。演示场景下conf-thres设 0.4 能过滤大部分背景误检也不会漏掉远处的目标。加上--save-txt会同时输出 YOLO 格式的检测结果文本方便后续统计检测数量或做精度对比。如果要跑摄像头实时识别把--source改成摄像头设备号比如 0python detect.py --weights best.pt --source 0 --conf-thres 0.4但这个依赖 OpenCV 版本和摄像头驱动USB 摄像头在 Windows 上有时打不开报Unable to capture frame。此时优先换一个 USB 接口或者把cv2.VideoCapture(0)改成cv2.VideoCapture(1)直接改源码即可定位。4.2 输出结果的可视化与效果评估推理结果保存在runs/detect/traffic_demo每张图会画检测框、类别名和置信度。我一般会挑三类典型场景截图放进报告单目标大标志、多目标小标志、夜间或暗光场景。TT100K 街景数据里有大量远处的小标志检测框可能把两个相邻标志识别成一个这就是--iou-thres 0.5引起的框融合。如果发现这种问题把阈值调高到 0.6。除了截图还可以用验证集输出混淆矩阵。YOLOv5 的test.py会自动生成confusion_matrix.png存放在runs/val目录。这个图能直观看到哪类标志容易被认成干扰。课程设计答辩时把混淆矩阵和 mAP 曲线放上去讲清楚“误差来源是类别不平衡还是小目标漏检”能显著提升报告的完整度。提示先跑test.py再跑detect.py前者的验证结果更可信后者只用来做展示素材。两个步骤顺序颠倒会影响你对模型真实水平的判断。5. 避坑指南五个最容易翻车的环节5.1 训练时报错 “AssertionError: train: No labels in ...”现象训练刚启动就报错提示train目录下没有标签。 原因traffic.yaml中train指向的train.txt里图片路径对应的标签文件不在labels目录或者标签文件名与图片名不一致。 解决检查train.txt中每行路径YOLOv5 会根据图片路径自动推导标签路径规则是把images替换为labels、扩展名替换为.txt。如果你的图片放在images/train标签就要放在labels/train目录层级必须对齐。换个思路最简单的方法是直接把train.txt里图片路径全改成绝对路径避免相对路径推导出错。5.2 训练正常但 mAP 始终低于 0.3现象训练了 100 个 epoch验证集 mAP 上不去模型预测结果几乎没有正确框。 原因最常见的是类别映射错误比如把多个不同含义的类别都设为同一个 ID导致模型学到的是混乱的语义。另一个可能的原因是标签归一化坐标使用了错误的图片宽高框的位置偏离真实目标。 解决先在detect.py推理几张训练集中的原图观察框和标注是否吻合。如果不吻合回到convert_annotation里确认图片真实尺寸用 PIL 动态读取from PIL import Image img Image.open(img_path) w, h img.size不要使用固定的 2048。标注框如果有偏差重新生成全部标签再训练。5.3 显存不足OOM 中断现象训练到第几个 batch 直接崩溃报CUDA out of memory。 原因--batch太大输入分辨率 640 时每张图占用显存较多yolov5l 和 yolov5x 在 12GB 显卡上几乎无法用 batch 32 训练。 解决把--batch降到 8 或 4同时把--workers降到 2。如果还需要降低显存用--img 416缩小分辨率或者换yolov5s权重。项目里如果提供.cfg文件也可以改成更小的网络深度。另外不要同时开着很多网页或 IDE 抢显存。5.4 验证集 mAP 很高但实际图片检测效果差现象验证集 mAP 0.85实测街头图片却各种漏检。 原因数据泄漏。随机切分训练集和验证集时TT100K 同一摄像头拍到的连续帧会被分到两边模型在训练时“见过”类似画面验证分数虚高。 解决严格使用 TT100K 官方的train和val目录划分而不是随机抽样。官方划分已经考虑了时间序列隔离按目录切分能避免这个问题。如果实在要用随机切分请以场景为单位切分不要以单帧为单位。5.5 修改代码后再次训练结果无法复现现象同一份数据、同一套参数第二次训练精度比第一次差 3 个点。 原因YOLOv5 的随机性来自随机初始化、数据加载的 shuffle 顺序以及 CUDA 的随机种子。 解决训练命令中加上固定种子参数并保证 GPU 不和其他进程共享显存python train.py --seed 42同时关闭--cache-images因为缓存数据的顺序可能在不同次运行中不稳定。如果你需要完全确定性把torch.backends.cudnn.deterministic设为 True并且在数据加载器里固定shuffle的生成器种子。课程报告里领域内允许 1~2 个点的波动但如果波动超过 5 个点优先检查是否换了显卡型号或 PyTorch 版本。6. 进阶从能跑到能讲——用消融实验和推理可视化提升项目说服力当前模型如果已经达到 mAP 0.7 以上课程设计或毕设答辩的基本盘就够了。剩下的时间用来做提升和论证而不是盲目调参。我可以分享一个投入产出比最高的进阶路径先做一次分辨率对比实验把--img从 640 改成 1280在验证集上重新评估TT100K 里有大量小目标分辨率提升对小目标召回率的影响会直接反映在数据上。这个实验能支撑一个论点“输入分辨率对交通标志检测精度有显著影响”比纯调conf-thres更有说服力。具体做法是训练两个模型一个 640 输入一个 1280 输入然后在同一验证集上比较 AP 曲线。1280 输入的模型在显存足够时用--batch 8即可。对比结果后我一般会把高分辨率模型的推理速度也测一下python test.py --weights best_1280.pt --data traffic.yaml --img 1280 --batch 16 --speed--speed参数会输出平均预处理时间、推理时间和 NMS 时间。把这一组数字放进报告说明“精度提升了 x%推理时间增加了 y 毫秒”方向就清晰了。如果时间不充裕只做这一组对比就够了。另一个实用技巧是导出 ONNX 再用 OpenCV 做一次推理。不是所有答辩环境都跑得动 PyTorch导出 ONNX 后可以用cv2.dnn加载模型做一个简单的桌面演示程序python export.py --weights best.pt --include onnx --img 640导出成功后用onnxruntime写一个推理脚本把检测框画到图上。这一步能显著提升项目完整度也让模型脱离 PyTorch 环境运行。导出时如果遇到算子不兼容问题比如某些自定义 NMS 层可以把--include改成只导出模型主体NMS 留在后处理里写。我每次跑这类项目都会强制自己走一遍完整链路数据转换 → 训练 → 验证 → 推理 → 导出 ONNX缺一环都觉得没做完。这个习惯让我在答辩或者交接时从来不会尴尬因为项目里的每一步产物都存在随时能复现。希望这份资源的实战拆解能帮你少走几趟弯路直接站到“能跑通、能讲清”的终点线上。本文还有配套的精品资源点击获取