ARTICLE DETAIL

资讯详情

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

基于YOLO的交通标志检测识别实战:从数据到部署

基于YOLO的交通标志检测识别实战:从数据到部署 简介面向智慧交通场景的交通标志检测与识别项目实战包以TensorFlow≥1.0.0构建卷积神经网络配合Numpy与easydict完成图像预处理和配置管理适合希望掌握深度学习视觉应用的初中级开发者。压缩包共247个文件主要包括63组meta/data/index模型权重相关文件、29个Python脚本网络定义、训练、评估与预处理、10个checkpoint断点及jpg样例图、txt说明等整体约54.95MB目录结构便于按模块学习脚本按数据预处理、模型搭建、训练评估分工checkpoint可配合恢复训练状态。已有181人学习下载。资源覆盖从数据集预处理、CNN特征提取到分类识别的完整流程并涉及滑动窗口/YOLO检测思路与迁移学习微调方法支持限速、禁令等多类常见标志的分类建模对自动驾驶辅助、智能交通系统等场景具有直接参考价值可用于课程设计、毕业设计或工程复现与二次开发。1. 交通标志检测与识别为什么一块路牌就是一个完整的人工智能项目夜间开着车摄像头要在几十毫秒内从画面上找到那块限速牌读出“60”再把结果交给规划模块——这个不起眼的动作拆开就是数据标注、目标检测、分类、模型压缩一整套流程。zip 工程包里装的就是这套以项目实践为标准组织好的代码和数据数据集、训练脚本、推理脚本一应俱全。你能直接复现也能改成自己的课程大作业或毕设选题。人工智能正从尝鲜工具变成日常帮手而交通标志检测与识别恰好是入门者能完整跑通、又不会被任务难度劝退的中间站。适合谁学过一点深度学习、想动手做第一个完整项目的学生或者正在给自己排人工智能学习路线、需要一次真实训练经历的人。这个项目的难点从来不在新网络结构而在数据处理的琐碎细节和部署时的边界情况。2. 先把数据立住交通标志数据集选型与标注格式转换2.1 主流数据集怎么选GTZDD、GTSRB、LISA 到底有什么差别拿到 zip 先别看代码看 data 目录这是血泪经验。交通标志检测与识别有两条技术路线老式做法是分类数据集配滑动窗口新式做法是检测数据集直接回归。你手里这套工程到底走哪条路从数据集格式就能判断。我一般会先判断数据集的类型。GTZDD 是国内常用中文路牌数据集绝大多数样本是中国标准的红圈白底限速牌和警告牌类别贴近本地智慧交通场景。LISA 是国外场景采集的路网标志光照差异大、背景复杂类别里有大量的箭头和路口标记。GTSRB 是经典的德国交通标志分类数据集图片量大、类别全但它只有单个标志的裁剪图没有边界框标注适合验证分类器不适合直接喂给 YOLO 做检测训练。这里有一个非常常见的误用有人拿到 GTSRB分类准确率刷到 99%就以为“交通标志检测与识别”跑通了。但分类和检测是两回事——检测要在整张图中先定位“哪里有标志”再做分类。拿分类数据集做检测项目相当于只做了一半。很多老项目包会把 GTSRB 当成训练集配一个滑动窗口去切图这是可行的但窗口尺寸、步长和缩放层级调起来非常烦效果还容易翻车。所以选型时记住一个原则目标是检测就优先找带框标注的数据集。GTZDD 和 LISA 都有 bndbox 形式的标注前者中文路牌接近本地场景后者国外场景适合测泛化。你可以做一个简单对比表来判断手里数据集适不适合当前项目。数据集任务类型类别构成适合场景GTZDD目标检测带框标注中文限速、警告、禁令标志国内智慧交通项目、课程作业LISA目标检测带框标注英文路标、箭头、信号标志泛化测试、光照差异实验GTSRB图像分类无框标注德国标志、样本均衡分类器训练、滑动窗口路线如果你打开 zip 发现 dataset 下是 JPEGImages 目录配 Annotation 目录的 VOC 风格结构那就是检测项目最常见的形式。下一步不是改模型而是把 VOC 的框坐标转成 YOLO 需要的格式。转换里藏着好几个能让你白忙半天的坑。2.2 把标注转成 YOLO 格式转换脚本与四个边界坑VOC 格式里xmin、ymin、xmax、ymax 是绝对值像素坐标YOLO 格式要的是归一化到 0~1 的中心点坐标和宽高。我一般会写这样一个转换脚本import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): # 解析 VOC xml输出 YOLO 的 txt 标注 tree ET.parse(xml_path) root tree.getroot() # 注意xml 里的 size 是原图尺寸不是导入到脚本里的图像尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_names: # xml 里可能出现背景类或未定义类别直接跳过 continue cls_id class_names.index(cls_name) # 类别索引必须从 0 开始 box obj.find(bndbox) # 先做 clamp防越界再做归一化 x1 max(float(box.find(xmin).text), 0) y1 max(float(box.find(ymin).text), 0) x2 min(float(box.find(xmax).text), img_w) y2 min(float(box.find(ymax).text), img_h) # 过滤退化框边已经反向说明标注本身有问题 if x2 x1 or y2 y1: continue x_c ((x1 x2) / 2) / img_w y_c ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h # 保留 6 位小数避免精度不足导致训练时框偏移 lines.append(f{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}) out_path os.path.join( out_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt ) with open(out_path, w) as f: f.write(\n.join(lines))这段代码里有四个边界坑都是实打实踩过的。第一类别索引从 0 开始且必须与后续 data.yaml 里的 names 顺序严格一致。如果 class_names 是 [limit_30, limit_60]那 limit_30 就是 0limit_60 就是 1顺序乱了对训练没影响对推理后处理的影响是灾难性的——你在部署时会把限速 30 的框打印成限速 60。第二归一化前先 clamp。原始标注经常会出现 xmin 是负数、xmax 大于图片宽度的情况尤其用标注工具手动框时边缘容易拖出画布。拖着不处理YOLO 在读取标签时会认为框坐标必须在 0~1 之间超界样本直接被丢弃现象是训练日志里提示 image has no labels。第三空标注文件必须保留。有的图片确实没有标志转换后 txt 是空文件这没问题。如果你顺手把空文件删了训练时数据加载会把“有图无标”误判成标注缺失部分框架会直接跳过该图导致训练集和验证集不一致mAP 曲线会很怪。第四浮点精度不要省。有的转换脚本用 round(..., 3) 只保留三位小数对 1280 宽的大图来说误差就有 1 像素以上对小目标检测是致命的。保留 6 位就够了。转换完还要做数据集划分。常见比例是 9:1 或 8:2我一般顺手在脚本里固定随机种子import os, random, shutil random.seed(42) # 固定种子保证每次划分结果一致可复现 images_dir JPEGImages labels_dir labels train_imgs, val_imgs [], [] for f in os.listdir(images_dir): if f.lower().endswith((.jpg, .jpeg, .png)): # 90% 训练 10% 验证 (train_imgs if random.random() 0.9 else val_imgs).append(f) for split, imgs in [(train, train_imgs), (val, val_imgs)]: os.makedirs(fimages/{split}, exist_okTrue) os.makedirs(flabels/{split}, exist_okTrue) for f in imgs: base os.path.splitext(f)[0] shutil.copy(os.path.join(images_dir, f), fimages/{split}/{f}) # 找不到 txt 就跳过不报错但要打印一条日志 src_label os.path.join(labels_dir, base .txt) if os.path.exists(src_label): shutil.copy(src_label, flabels/{split}/{base}.txt) else: print(fWARNING: missing label for {f})固定随机种子这个习惯值得培养。同样的数据、同样的代码隔天跑出来的结果不一样多半就是划分时没固定种子训练集和验证集的构成变了。你拿这个项目去答辩或写报告时可复现性是加分项。数据就绪后下一步才是选模型。很多人一上来就纠结该用 YOLOv5 还是 YOLOv8实际上在交通标志这类任务上模型选择受数据规模和部署硬件的约束比受网络结构差异的约束大得多。3. 模型选型与训练配置从 YOLOv8 到轻量级 Backbone 的取舍3.1 为什么首选的还是 YOLO 系智慧交通场景里的识别任务几乎都要求实时。摄像头装在车上一秒钟要处理 25 到 30 帧留给单帧推理的时间只有 30 到 40 毫秒。这样一个硬约束直接把两阶段检测器排除在外。Faster R-CNN 家族精度确实漂亮但在边缘设备上跑不到实时SSD 是单阶段老将训练生态远不如 YOLO 系活跃。我一般会在 YOLOv8n 和 YOLOv8s 之间选。交通标志这个任务有两个特点目标小、类别语义简单。限速牌和警告牌在画面里往往只有几十个像素宽模型不一定要很深类别通常只有十几到几十类区分“限速 30”和“限速 40”靠的是文字细节不需要特别大的感受野。轻量网络的精度损失在可控范围换来的是推理速度和部署方便这是性价比最高的组合。如果你手里的项目包是老代码用 VGG 加滑动窗口做分类我的建议是直接替换成 YOLO 系不要在老路线上修修补补。常见做法是保留老代码里的数据预处理流程把检测头换成 YOLO 的单阶段输出训练时间大幅缩短效果还会更好。当然老代码的价值在于理解从候选框、特征提取到分类回归的完整链路所以如果这是教学作业而不是工程交付先读懂再替换也不迟。3.2 最小可复现训练配置一个 data.yaml 和一串训练命令用 ultralytics 训练 YOLOv8配置文件是 data.yaml。结构很简单但 namespace 很容易写错我见过不下三个人把 train 路径的缩进写错导致训练时读不到图。# data.yaml路径配置和类别定义 # path 是数据集根目录最好写相对路径方便换机器复现 path: ./jhh train: images/train val: images/val # nc 必须和 names 长度一致否则框架直接报错 nc: 30 names: [speed_limit_30, speed_limit_40, speed_limit_60, speed_limit_80, no_entry, no_parking, warning_curve_left, warning_curve_right, # ... 其余类别按 class_names 的顺序补全 ]names 的顺序就是第 2 章转换脚本里 class_names 的顺序两者必须一一对应。一个常见低级错误是这里把 names 重新按字母排序于是类别索引全乱了训练时每个类名和真实框错位模型越训越糊涂。记得data.yaml 不是给你看的是给代码看的它只认索引。训练命令我一般这么写yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ device0几个参数值得说明一下。imgsz 用 640不要为了追求小目标精度盲推到 1280。交通标志虽然是中小目标但 YOLO 在 640 下对大部分标志已经能出稳定的框1280 会把训练时间拉长三倍显存占用也翻倍收益只在远距离小目标上有性价比不高。如果你的验证集里确实有大量极远距离的小标志可以把 imgsz 提到 960但 batch 要相应降到 8否则 OOM 是跑不掉的。batch 先按显存定。常见的 8G 显存跑 yolov8nbatch 16 能过12G 显存可以上 32。不要迷信大 batchbatch 太大在 YOLO 训练里的提升有限倒是 batch 太小导致 BN 统计不稳定Loss 曲线会像锯齿一样抖。patience 是早停耐心值。epochs 设 100如果 loss 连续 20 个 epoch 没有下降训练提前结束。这样就不用死守 100 个 epoch能省不少时间。device 用 0 表示第一块 GPUCPU 跑训练可以换成 devicecpu但速度会慢到让你怀疑人生不建议在 CPU 上等 100 轮。训练完成后看什么先看 runs/detect/train 下的 loss 曲线和混淆矩阵。YOLO 的 train_loss 下降是正常的关键要看 val_loss 是否跟着下降两者背离就是过拟合信号。混淆矩阵能告诉你哪两类互相认错——交通标志任务里最常见的错误是限速 30 和限速 40 互相混因为文字细节太接近这种错靠调参很难根治要靠数据补样。这段路线跑通以后你会发现一个现象训练本身不是瓶颈瓶颈在于我不知道模型到底学到什么程度算“能用”。那需要另一套评估视角我一般在训练结束后不做单次验证而是做一组有产出的推理测试这也正是第 4 章的内容。4. 训练评估与推理验证从 Loss 曲线到 mAP 各项指标怎么看懂4.1 评估指标怎么取舍mAP0.5 够用吗训练结束后终端会输出一串指标很多人只看 mAP0.5。这个指标对工程验收够用但对智慧交通场景其实不够敏感。指标含义落地里的取舍mAP0.5IoU 阈值 0.5 下的平均精度工程够用好解释答辩友好mAP0.5:0.95IoU 从 0.5 到 0.95 取平均更能反映框的精确度科研常用Per-class AP单类别平均精度排查“哪类在拖后腿”的必备指标Precision/Recall精确率与召回率看漏检和误检更直观交通标志检测的核心评价标准是召回率。漏检一块限速牌比误检一块空广告牌更严重因为下游规划模块拿不到该有的速度约束。所以我一般会额外看 per-class recall尤其是限速类的 recall。如果某个类别的 recall 在 0.8 以下就得回去查这一类的样本数量和标注质量。mAP0.5 和 mAP0.5:0.95 的差距也值得注意。如果前者高、后者明显低说明模型能出框但框不够精确边界框偏移明显。交通标志场景对身份匹配要求高文字信息在框的边缘位置框偏了会把“60”和“80”的局部特征裁掉直接影响分类结果。遇到这种情况优先考虑把 imgsz 调大或者换更深的模型而不是去调 NMS 阈值。我还建议训练完后导出一个评估结果表按类别列出 precision、recall、mAP0.5。这样做的好处是答辩或项目汇报时能直接指出问题在哪、下一步改进方向是什么而不只是说一句“mAP 达到 0.9”。4.2 写一个推理脚本图片、视频、摄像头三种输入评估完成下一步是让模型跑一个真实推理。ultralytics 提供了统一的 predict 入口但参数得说清楚否则要么不出框要么出框太多。from ultralytics import YOLO # 加载训练时保存的最佳权重不是 last.pt model YOLO(runs/detect/train/weights/best.pt) # 准备一个测试图目录存一些训练时没见过的实拍图 results model.predict( sourcetest_imgs/, # 支持目录、单张图、视频流 conf0.4, # 置信度阈值 iou0.45, # NMS 的 IoU 阈值 imgsz640, # 必须和训练时一致否则结果会变差 saveTrue, # 把画了框的结果图存下来 save_txtTrue # 同时输出 txt 格式的结果 ) # 遍历结果打印每个框的类别、置信度和坐标 for result in results: for box in result.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) xyxy [round(v, 2) for v in box.xyxy[0].tolist()] print(result.names[cls_id], conf, xyxy)这段代码的要点是 conf 参数。训练时默认的置信度阈值是 0.25推理时我用 0.4原因是在交通标志场景里误检的代价比漏检低——车机会结合上下游逻辑再过滤一次。如果你发现测试图片上有很多框是背景误报把 conf 提到 0.5 即可如果你发现明显的标志没被检出来需要降低 conf 而不是怀疑模型坏了。imgsz 参数是另一个容易翻车的点。训练用的 640推理也必须是 640。训练用的 960推理用 640输入分辨率差异会导致特征图的尺度不匹配mAP 掉 5 到 10 个点是常事。遇到“模型训练 mAP 挺高推理效果稀烂”的情况先检查 imgsz 对不对。摄像头输入就是把 source 改成 0即设备默认摄像头yolo detect predict modelruns/detect/train/weights/best.pt source0 conf0.4 showTrue视频输入换成视频文件路径即可。这里的 showTrue 只是调试用真正部署要关掉显示窗口省掉画框和显示的耗时。到这里一个常规项目已经能从数据跑到推理。但真正让“某某检测与识别”类项目拉开差距的是那些不写在 README 里的坑。这些坑我在反复的实验里攒了不少单独列出来。5. 避坑指南交通标志检测从数据到部署的五个常见问题与排查5.1 类别样本不平衡标志种类多样本全堆在“限速”上现象训练完后 per-class AP 差距很大限速类 0.9警告类 0.3可明明总体的 mAP 看着还过得去。原因真实路况里限速牌出现的频率远高于警告牌人工标注时顺手多标了限速类类别分布天然倾斜。往大了说这就是人工智能里常见的数据偏见问题模型学到的是“多数类更好检”而不是“这类标志长这样”。解决先统计各类别的样本数低于平均水平的类别做过采样即复制该类别样本重复训练简单有效但不要复制太狠再叠加基础图像增强把少数类做随机裁剪、平移、旋转制造更多变体。最直接的办法是去数据集中找包含少数类的新图而不是依赖增加训练轮数。训练轮数越多模型越偏向多数类这个反直觉结论值得记住。5.2 标注框越界被自动过滤导致训练集缩水现象训练日志里有一行警告image 1/100: 0 labels但人眼看着图片明明有标志。或者是验证时边缘目标大量漏检。原因标注工具里框拖出了图像边界坐标变成负数或超过图像宽高。YOLOv8 的标签加载器会把超界样本当成无效数据直接丢弃于是这些图成了“有图无标”既浪费数据又干扰训练。解决写第 2 章那种转换脚本时对 xmin、ymin、xmax、ymax 做 clamp把坐标限制在 [0, img_w] 和 [0, img_h] 内。再补一条规则如果 clamp 后宽或高小于 5 像素直接丢弃该框这种退化框对训练毫无帮助。一个附加习惯是把越界情况打印成 warning方便回看标注质量。5.3 夜间和逆光泛化差白天 mAP 高晚上几乎漏检现象白天测试图 mAP 0.85晚上同一批分类别的测试图 mAP 0.3模型像换了个人。原因训练集里夜间和逆光的样本太少模型学会的是“白天光照下的特征组合”而不是“标志本身的结构特征”。标志是反光材料不同光照下成像差异极大没有对应训练样本神经网络就学不到这种不变性。解决增强策略里加上随机亮度扰动、Gamma 变换和 HSV 饱和度扰动这比加大量亮度曝光样本成本低很多。再针对性补一组夜间实拍图哪怕只有几十张配合 mosaic 增强也能把夜间 recall 拉回来。另一个小技巧是推理时对输入做自适应的直方图均衡化让暗部细节更清晰但这种预处理会带来额外耗时部署到边缘设备前要实测一次帧率。5.4 Loss 曲线正常但 mAP 为 0先查 val 标签是不是空的现象训练 loss 一路下降验证集 mAP 第一轮开始就是 0之后纹丝不动。原因大概率不是模型坏了是训练时验证集的标签没配对。常见是数据划分脚本把图片复制到了 val 目录标签却因为文件名不匹配留在了原处或者 val 下的 txt 文件全都没生成。模型在训练集上学得再好验证时看不到正确答案mAP 就是 0。解决训练前先写一个小脚本统计 train 和 val 两个目录里图片数与 txt 数是否一致不一致直接终止训练。再单独跑一次 predict 看单张图有没有框输出如果单张图有框而 mAP 是 0那问题一定在验证标签路径不在模型。5.5 推理速度远低于预期部署帧率上不去现象训练和推理在同一台机器上GPU 推理单帧只有 8 到 10 FPS离部署要求的 25 到 30 FPS 差一大截。原因常见有三个。一是输入图太大imgsz1280 推理算力都耗在卷积上二是模型没导成推理格式PyTorch 原生模型带很多额外开销三是机器本身是低功耗的边缘盒子GPU 算力有限跑不动 YOLOv8s。解决先把 imgsz 降到 640然后用 ONNX 导出再叠加 TensorRT 做 FP16 量化。YOLOv8 系模型转 ONNX 后用 TensorRT 推理通常能比 PyTorch 原生快两到三倍。如果还达不到实时就换 YOLOv8n。FLOPs 和帧率的取舍在部署阶段永远站在帧率这边。记得量化后重新跑一遍验证集检查 mAP 掉点是否在可接受范围掉点超过两个点就说明量化策略需要调整。这五个坑覆盖了数据、训练、评估、推理和部署五个环节项目里任何一个环节出问题排查优先级都比换模型结构高。6. 最后一步把检测结果结构化输出接进你的业务代码训练和推理都通了项目要从“跑出框”升级到“能被业务系统使用”。常见做法是把检测结果封装成结构化 JSON而不是直接输出画了框的图片。下游的速度限制模块、数据统计模块需要的都是“什么类别、置信度多少、框在哪”这样的字段。import json def detect_to_json(results): 把 YOLO 的 Results 对象转成结构化 JSON 列表 items [] for result in results: for box in result.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) xyxy [round(v, 2) for v in box.xyxy[0].tolist()] item { class: result.names[cls_id], conf: conf, bbox: xyxy, # 当前帧号视频流里用于判断同一标志是否被重复识别 frame_id: getattr(result, frame_id, 0) } items.append(item) return json.dumps(items, ensure_asciiFalse)这里 frame_id 值得展开说。视频流里同一个交通标志会连续出现在几十帧里如果你不做去重下游模块会收到几十条重复结果逻辑很难写。我的做法是给结果加一个“静默期”同一类别、同一位置的框在 500 毫秒内只输出一次超过静默期再允许输出。这个逻辑放在业务层比放在模型层更合适因为模型层不该关心时序。验证推理效能的技巧也在这里准备一段 3 分钟实拍视频每帧跑一次检测记录漏检帧、误检帧和每一类别的输出次数形成一份基准报告。之后每次调整参数都跑同一段视频对比报告变化。这个习惯帮我避免了很多次“改完参数以为变好了、实际是换了测试数据”的假象。我入这行最大的教训就是先复现、再调参、后换网络。很多次项目延期都源于不甘心现有结果一上来就换模型结果数据问题和部署问题还在原地。先把这个方向的闭环做扎实再去碰更花哨的结构。希望帮到你。本文还有配套的精品资源点击获取
返回列表