ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv5+TT100K交通标志识别全流程指南:从数据转换到部署

YOLOv5+TT100K交通标志识别全流程指南:从数据转换到部署 简介一套基于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数据怎么喂给yolov5json转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是类别字符串IDbbox是像素坐标的左上角和右下角。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(parentsTrue, exist_okTrue) with open(json_path, r, encodingutf-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), encodingutf-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/valimg_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 python3.9 -y conda activate tt100k # 先装pytorch再装yolov5依赖顺序不要反 pip install torch2.0.1 torchvision0.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是2nc就是3names的顺序必须和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还在涨就继续加。日志主要看三个数mAP0.5、mAP0.5:0.95和val/obj_loss。前20轮mAP不涨是正常的迁移学习刚开始在适应新数据分布如果80轮后mAP0.5还不到0.5基本是数据转换或划分有问题回头查第2章。4.2 对交通标志牌影响最大的几个超参数参数默认值建议值原因imgsz640640或1280标志牌小目标多高分辨率能显著涨点batch16按显存最大设置batch过小会让batch norm统计不稳定lr00.010.01~0.02学习率过大容易loss变nanclose_mosaic1010~20最后n个epoch关闭mosaic让模型适应真实分布anchor自动不手动改yolov5会在训练集上重新聚类anchorimgsz从640调到1280小目标召回率通常能提升5到10个点代价是显存翻倍、训练时间翻倍。close_mosaic是yolov5官方后期加入的trickmosaic增强会把四张图拼在一起导致小目标被缩放得面目全非最后若干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或者训练了几十个epochmAP0.5始终是0。 原因最常见的是标签里有0宽度或0高度的框或者类别ID超出了yaml里nc的个数。 解决写脚本检查labels目录里每个txt凡是w或h为0的行直接删掉检查class ID是否小于nc再用cv2.imread逐张检查图像读不出来就跳过。这个坑几乎每个第一次做数据转换的人都会遇到检查一遍就通。坑2验证集mAP很高但换一批图片检测效果很差 现象随机划分的val上mAP0.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图像分辨率不足导致小目标漏检 现象远处的限速牌检不出来走近才出框。 原因原图1920x1080yolov5缩到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的CPUyolov5s加640输入单帧推理约1.5到3秒基本不可实时。常见做法是把模型换成yolov5nimgsz降到320用onnxruntime的多线程跑单帧约300到500毫秒配合跳帧检测能到10fps左右。如果你的板子有NPU比如瑞芯微或Jetson系列int8量化才有明显加速纯CPU上int8量化收益有限主要作用是减小内存占用。精度对比上yolov5s从fp32量化到int8mAP0.5通常会掉2到5个点对交通标志识别来说还能接受。这个项目做到最后我最大的教训是不要为了帧率盲目降imgsz。把320的模型部署到真实路口结果漏掉了30米外的限速牌项目差点被打回。后来改成640输入加跳帧检测才把速度和准确率的账算平。如果你想把这个方案放进自己的项目建议先定场景课程设计就用best.pt跑demo面向真实路口就要在imgsz和后处理上下真功夫。希望帮到你。本文还有配套的精品资源点击获取
返回列表