☰
交通标志检测与识别实战:从YOLOv8训练到边缘部署
2026/10/1 4:07:19 网站建设 项目流程

简介:这套基于TensorFlow的交通标志检测与识别实战项目,聚焦人工智能在智慧交通领域的应用,适合具备一定Python基础、希望系统学习计算机视觉与深度学习的开发者和学生。压缩包为zip格式,共247个文件、约54.95MB,其中包含63组模型权重文件(meta/data-00000-of-00001/index)、29个Python脚本、10个pyc与checkpoint、5张示例图片和4个文本文件,覆盖从数据预处理、模型构建、训练到评估的完整工程链路。项目使用Python 3.5编写,依赖TensorFlow(≥1.0.0)、Numpy和easydict,代码中展示了卷积神经网络的特征提取与多类分类过程,并包含可复用的训练与推理脚本。已有181人学习,资源不仅提供了可直接加载运行的模型参数和完整项目目录,还能帮助学习者快速理解从检测区域定位到多类分类的完整技术链路,对于自动驾驶、智能交通等方向的实战演练具有较高参考价值。

1. 交通标志检测与识别:这个 zip 里装的不只是模型权重

拿到手的是“人工智能-项目实践-智慧交通-交通标志检测与识别.zip”,解压之后往往是一堆 Python 脚本、yaml 配置、训练日志,运气好还有几个 .pt 或 .onnx 权重文件。作为人工智能项目实践里最经典的智慧交通课题,交通标志检测识别解决的是“车看到什么、该听谁的”这个物理层问题:先定位画面里的标志牌,再判断它是限速、禁行还是警告。很多学生和刚入行的工程师把它当作大作业,但真正落过地的人都知道,从 zip 里的代码到车上能跑,中间隔着数据清洗、小目标漏检、类别不均衡和边缘设备算力四道坎。这篇笔记按我自己的实操顺序,把数据集准备、模型训练、识别头设计、部署调优和常见坑一次讲透,给你一套能复现、能改参数的完整路径。

2. 数据集与标签格式:先搞懂你的 zip 里哪些能用,哪些要重写

很多压缩包里给的样例数据只有几百张图,直接拿来训练会过拟合到只剩“认识示例图”的程度。交通标志检测的第一步不是写模型,而是把数据源头理清楚,否则后面所有指标都是自欺欺人。

2.1 公开数据集怎么选:TT100K、CCTSDB 还是自采

常见做法是先用公开数据集跑通基线,再补自采数据。主流有三个:

  • TT100K(清华-腾讯 100K):基于腾讯街景图像,包含上万张街景图和数万标注框,适合复杂背景、小目标场景。标注是 JSON 格式,标志类别用字符串表示,如“p130”(限速 130)、“i5”(禁止驶入)。需要申请下载,通常学校邮箱可过审。
  • CCTSDB(长沙理工大学交通标志数据集):包含真实道路场景下的标志标注,有白天、夜晚、逆光等变化,格式偏 VOC/自定义 TXT,适合初学者。
  • GTSRB(德国交通标志识别数据集):主要用于分类而非检测,单张图就一个标志,适合做识别头微调,不适合直接做检测训练。

我一般建议以 TT100K 为主,因为它更接近“智慧交通”的真实街景分布——同一块牌子在图像里可能只有 20×20 像素,这正好暴露检测模型的短板。但 TT100K 的类别分布极不均衡,比如“限速 30”和“限速 120”数量差一个量级,训练时需要在采样策略上做文章。

2.2 标签格式转换:把 VOC XML 或 TT100K JSON 转成 YOLO txt

YOLO 系列用的是归一化后的 txt 格式:每行class_id x_center y_center width height,坐标是相对于图片宽高的比例。TT100K 原始 JSON 里给的是左上角坐标和宽高,必须先转换。

下面这个脚本可以直接处理 TT100K 的 JSON 标注,输出 YOLO 格式到labels目录:

import json import os from pathlib import Path def convert_tt100k(json_path, img_dir, label_dir, class_map): Path(label_dir).mkdir(parents=True, exist_ok=True) # TT100K 的标注文件按图片名组织,如 annotations/{img_id}.json for json_file in Path(json_path).glob("*.json"): with open(json_file, encoding="utf-8") as f: data = json.load(f) img_name = data["name"] # 例如 "42_30_10.jpg" img_w = data["width"] img_h = data["height"] lines = [] for obj in data["objects"]: # TT100K 里有些对象被标记为 difficult,一般跳过 if obj.get("difficult", False): continue cls = obj["category"] if cls not in class_map: continue box = obj["bbox"] # [x_min, y_min, x_max, y_max] x_min, y_min, x_max, y_max = box x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h box_w = (x_max - x_min) / img_w box_h = (y_max - y_min) / img_h # 边界框剪裁到 [0,1],避免越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) box_w = min(max(box_w, 0.0), 1.0) box_h = min(max(box_h, 0.0), 1.0) lines.append(f"{class_map[cls]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") label_path = Path(label_dir) / (Path(img_name).stem + ".txt") label_path.write_text("\n".join(lines), encoding="utf-8") print(f"转换完成,标签保存在 {label_dir}") # 示例:把类别名映射成从 0 开始的 id class_map = {"p130": 0, "p120": 1, "i5": 2, "i4": 3, "w30": 4} convert_tt100k("annotations", "images", "labels", class_map)

这里的逻辑要点是:先读 JSON 得到宽高和 bbox,再做归一化。为什么转成 YOLO 格式而不是直接用 JSON?因为ultralytics库的data.yaml只认 txt,而且训练时的 Mosaic 增强需要对每张图的标签做随机拼贴,txt 格式读取最快。转换时还要注意:TT100K 有些 bbox 的坐标会略超出图像边界,必须用min/max裁到 [0,1],否则训练时 loss 会变成nan或拉框位置失控。

2.3 图像预处理参数:尺寸、归一化、增强的推荐值

YOLOv8 默认用 640×640,但对交通标志这种小目标场景,我建议至少 800×800,有条件直接 1024。原因很好理解:一块 40×40 的标志在 640 缩放下变成 25×25,特征图上的感受野已经很难分辨字符细节;放大到 1024 后,网络能捕捉到“限速数字”的边缘纹理。

归一化直接用ultralytics内置的 RGB 除以 255 即可。增强方面,交通标志不依赖旋转不变性——你很少看到倒着的限速牌——所以fliplr=0.5可以开,但flipud=0和rotate>10都建议关掉。真实场景里标志牌可能因视角产生一定倾斜,我一般会把shear=5、translate=0.1、scale=0.3配合使用,模拟车辆行驶时的透视变化。另外,HSV 增强值得打开,因为早晚光线下标志颜色的饱和度差异很大,hsv_h=0.015、hsv_s=0.5、hsv_v=0.4能让模型对光照更鲁棒。

3. 模型选型与训练:用 YOLOv8 跑通最小可用的检测器

交通标志检测的模型选型,本质上是在“精度”和“边缘设备推理速度”之间做权衡。很多教程一上来就让你改骨干网络,但如果你还没跑通过一个标准训练流程,那些改动只会带来排错灾难。

3.1 为什么选 YOLOv8 而不是 Faster R-CNN

Faster R-CNN 的两阶段结构在精度上有优势,但推理速度对智慧交通的视频流不友好。同样处理 1080p 视频,YOLOv8n 在 Jetson 类设备上可以跑到 30 FPS 以上,而 Faster R-CNN 通常只有个位数 FPS。此外,YOLOv8 的工程化程度高:它内置了 Mosaic 增强、自动锚框、EMA 和一系列训练日志回调,你不需要自己写数据加载器和 NMS 后处理。

我用 YOLOv8 还有一个原因:它的检测头是解耦的,分类和回归分支分开输出,对小目标更友好。交通标志常常只有几十像素,解耦头能避免“分类 loss 和回归 loss 互相拉扯”的问题。如果你在 zip 里看到的是 YOLOv5,也可以按类似流程跑,但 v8 在训练稳定性上更好,mAP@0.5:0.95普遍高 2~3 个点。

3.2 训练配置:yaml 文件与超参数设定

先建一个data.yaml,放在你的数据集根目录下:

# data.yaml path: /path/to/tt100k_root train: images/train val: images/val test: images/test nc: 5 # 类别数,按你自己的 class_map 调整 names: 0: "speed_limit_130" 1: "speed_limit_120" 2: "no_entry" 3: "no_stopping" 4: "warning_30"

然后通过命令行或 Python API 启动训练。我一般用 Python 脚本,方便后续断点续训和调参:

from ultralytics import YOLO # 加载预训练权重,nc 会从 data.yaml 自动覆盖 model = YOLO("yolov8s.pt") results = model.train( data="data.yaml", epochs=120, imgsz=1024, batch=16, lr0=0.01, lrf=0.01, optimizer="SGD", # 用 SGD 而不是 Adam,泛化更好 project="runs/tt100k", name="exp1", patience=15, hsv_h=0.015, hsv_s=0.5, hsv_v=0.4, translate=0.1, scale=0.3, shear=5, flipud=0.0, fliplr=0.5, )

这里几个参数的考虑:lr0=0.01是 SGD 的常用初始学习率,配合Cosine调度器到训练后期降到lrf=0.01。patience=15表示验证集 mAP 连续 15 轮不涨就提前停,能省时间。batch=16在 1024 分辨率下至少需要 16GB 显存,如果只有 8GB,把batch降到 8 或imgsz降到 800。还有一个隐藏参数是close_mosaic=10,最后 10 个 epoch 关掉 Mosaic,让模型脱离拼接图像、适应正常比例,这个小动作能稳定提升 mAP。

3.3 训练命令与日志观察:mAP 到底看哪个

训练跑起来后不要只盯着 loss。loss 下降曲线很容易骗人——它只能反映拟合程度,不能反映检测质量。真正决定模型能不能用的是验证集上的mAP@0.5和mAP@0.5:0.95。前者是 IoU 阈值 0.5 时的平均精度,偏向“框得差不多就行”;后者从 0.5 到 0.95 每步 0.05 取平均,对框的位置精度更苛刻。交通标志部署场景看mAP@0.5:0.95更可靠,因为标志框偏移哪怕 2 个像素,字符识别都可能出错。

训练日志里还要关注val/cls_loss和val/dfl_loss。如果分类 loss 降了但回归 loss 震荡,大概率是锚框匹配出了问题;如果两个 loss 都在降但 mAP 不动,可能是数据标注有噪声——例如 TT100K 里有些框只框住标志牌的一半,模型学到的边界不统一。

训练完成后用model.val()看每类精度,尤其注意样本少的类别。如果某个类别的mAP@0.5低于 0.6,别急着调模型,先回看这一类在训练集里的标注数量。我见过一个项目里“禁止停车”标志只有 87 个框,训练完完全没被学到——这不是模型 bug,是数据分布问题。

4. 智慧交通里的识别:检测框之后的分类头怎么接

检测是“把标志框出来”,识别是“把框里的标志认成具体类别”。很多项目把这两个任务混在一个模型里——YOLO 的每个锚框都直接输出类别概率,这确实是检测识别一体。但真实智慧交通场景中,大量交通标志是相似类别,比如限速 40 和限速 30 只差一个数字,单阶段检测器在这种细粒度区分上往往不够稳。

4.1 检测与识别的区别:一个框和一个标签

在 YOLO 这类单阶段模型里,分类分支对每个候选框直接预测所有类别的概率,这个做法对小目标和相似类别不友好。原因在于:分类和回归共享特征,而小目标的特征图分辨率极低,数字细节可能已经在下采样时丢失。所以常见的工程做法是“粗检测 + 细分类”:先用 YOLO 输出所有标志框,再用一个轻量分类网络对每个框做二次判断。

这个思路的落地结构大概是:

  1. YOLO 检测,输出所有标志框,置信度阈值设低一点(0.25),保证召回。
  2. 按框裁剪原图对应区域,缩放到 48×48 或 64×64。
  3. 送入一个 ResNet18 或 MobileNetV3 分类器,输出细类别。

这样做的价值在于:分类器可以单独用 GTSRB 这类高质量分类数据集微调,避开检测数据集类别不均衡的坑。比如 TT100K 里限速 100 的样本少,但 GTSRB 有大量限速标牌,分类头学得更充分。

4.2 小目标重识别:裁剪、放大、二次分类的实战组合

下面这段伪代码给出了二次分类的完整流程,我直接用在边缘设备上没有调结构,只改了输入尺寸:

import cv2 import numpy as np import onnxruntime as ort # 加载检测模型和分类模型 det_session = ort.InferenceSession("yolov8s.onnx", providers=["CUDAExecutionProvider"]) cls_session = ort.InferenceSession("cls_resnet18.onnx", providers=["CUDAExecutionProvider"]) def recognize(frame, det_session, cls_session, conf_thres=0.25): # 1. YOLO 检测,得到 boxes, scores, class_ids boxes, scores, class_ids = detect(frame, det_session, conf_thres) results = [] for box in boxes: x1, y1, x2, y2 = [int(v) for v in box] # 2. 裁剪并外扩 20% 像素,避免边缘截断 h, w = frame.shape[:2] pad_x = int((x2 - x1) * 0.2) pad_y = int((y2 - y1) * 0.2) x1 = max(0, x1 - pad_x); y1 = max(0, y1 - pad_y) x2 = min(w, x2 + pad_x); y2 = min(h, y2 + pad_y) crop = frame[y1:y2, x1:x2] if crop.size == 0: continue # 3. 缩放并归一化,注意分类模型训练时的预处理参数 crop = cv2.resize(crop, (64, 64)) crop = crop[:, :, ::-1] # BGR -> RGB crop = crop.astype(np.float32) / 255.0 mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) std = np.array([0.229, 0.224, 0.225], dtype=np.float32) crop = (crop - mean) / std crop = np.transpose(crop, (2, 0, 1))[None, ...] # 4. 分类器输出细类别 logits = cls_session.run(None, {cls_input_name: crop})[0] cls_id = int(np.argmax(logits)) # 5. 用分类结果覆盖检测结果中的粗类别 results.append((box, cls_id, float(np.max(logits)))) return results

这段代码里最关键的参数是外扩比例0.2。YOLO 的检测框通常紧贴标志轮廓,如果直接裁剪用于分类,数字可能被切掉一部分,导致限速 60 被认成 30。外扩 20% 能把边缘留出来。另外,分类模型的预处理必须和训练时完全一致,否则精度掉 5 个点以上。这里用的是 ImageNet 的 mean/std,如果你用 GTSRB 从头训练,要重新统计自己数据集的均值方差。

这样做的好处是召回和精度都更稳,代价是推理耗时增加。在 Jetson Nano 上,YOLOv8s 检测一帧约 40ms,ResNet18 处理一个框约 3ms,如果一帧里有 10 个标志,总耗时到 70ms,勉强能跑到 14 FPS。如果对帧率要求高,可以把分类器换成 MobileNetV3-Small,单框降到 1ms 以内。

5. 交通标志检测常见坑与排查:从 zip 解压到部署的 5 个血泪经验

这个章节写的都是我在实际项目中踩过的坑,每一条都对应真实事故。按“现象 → 原因 → 解决”的顺序列出来,你遇到类似问题时可以直接照方抓药。

5.1 zip 伪加密导致的数据集解压失败

现象:从网上下载的交通标志数据集压缩包,解压时提示“文件损坏”或“密码错误”,换了好几个解压软件都失败。

原因:有些分享者用 WinRAR 或 7-Zip 设置了伪加密,也就是只在 zip 目录头标记了加密位,但实际文件内容没加密。解压软件检测到加密标志就去要密码,而真正的密码可能不在页面描述里。

解决:遇到这种情况,不要急着找密码。用 7-Zip 打开压缩包,如果能看到文件列表但提取需要密码,可以尝试以下命令修复目录头(常见于伪加密):

# 从 zip 中读取本地文件头,把加密标志位从 1 改回 0 python -c " import zipfile with zipfile.ZipFile('dataset.zip') as z: for name in z.namelist(): print(name, 'crc=', z.getinfo(name).CRC) "

如果 zipfile 能正常列出文件,说明只是伪加密,可以用zip -FF dataset.zip --out fixed.zip尝试修复目录头,然后正常解压。如果 zipfile 也报错,那就是真加密或文件损坏,只能重新找资源。这个坑很隐蔽,我第一次遇到时以为是下载不完整,白下了三遍数据。

5.2 训练时 loss 变成 nan

现象:训练跑到第 23 轮时,box_loss 突然变成 nan,随后所有指标全部失效。

原因:最常见的元凶是学习率过高导致梯度爆炸,或者数据中出现了空标签文件。YOLO 训练时如果某张图的 txt 标签是空文件,虽然会跳过该图,但 Mosaic 增强会随机组合多张图,空标签会导致拼接后的标签索引越界,触发 nan。

解决:训练前先跑一遍标签校验脚本,把所有空标签和对应图片找出来。同时把初始学习率从 0.01 降到 0.005,并开启amp=True(混合精度),能有效抑制梯度爆炸。我建议把下面的校验脚本集成进数据预处理流程:

from pathlib import Path label_dir = Path("labels") img_dir = Path("images") empty_files = [] for txt in label_dir.glob("*.txt"): lines = txt.read_text().strip().splitlines() if not lines: empty_files.append(txt.stem) print(f"空标签文件:{len(empty_files)} 个") # 打印前 20 个,然后决定是否删除对应图片 for name in empty_files[:20]: print(name)

5.3 小目标漏检严重,mAP 很高但实测看不见远处的标志

现象:验证集 mAP@0.5:0.95 到了 0.78,看起来不错。但把模型放在路口视频里测试时,30 米外的限速牌完全检测不到。

原因:验证集里的图像分辨率大多在 1024 以上,但实际视频流分辨率可能只有 1080p 甚至 720p。模型在训练时看到的标志尺寸占比和部署时不一样,产生了尺度偏移。还有一个常见原因:训练时imgsz=1024,部署时为了帧率降到了 640,导致小目标特征直接消失。

解决:部署推理尺寸必须和训练尺寸保持一致,至少相差不能超过 1.5 倍。如果边缘设备算不动 1024 分辨率,就用 800 重新训练一个模型,别指望 640 的推理能搬回 1024 的训练精度。另外可以开augment=True的测试时增强,但帧率会下降一半,我一般只在离线检测时开。

5.4 夜间场景让检测模型集体翻车

现象:白天指标正常,到了黄昏或夜间,检测框开始乱跳,甚至把车灯当作标志框。

原因:公开数据集大多是白天光照采集的,模型学到的颜色特征在低照度下失效。YOLO 的 HSV 增强虽然能模拟亮度变化,但模拟范围有限,真实夜间的高动态范围(高光车灯、阴影)很难增强出来。

解决:最有效的办法是在训练集里混合 20%~30% 的夜间图像。如果手上没有,可以先用图像处理工具做 gamma 校正和对比度拉伸,把白天图暗化成伪夜间图,但效果有限。更可靠的是用 AutoAugment 或 RandAugment 加入BrightnessTransform,把亮度下限拉到 0.3。部署时还可以在摄像头管线上加自动曝光和降噪,比让模型硬扛更稳。

5.5 导出 ONNX 后检测结果和 PyTorch 不一致

现象:PyTorch 模型 FPS 和 mAP 正常,导出成 ONNX 后用 TensorRT 跑,发现在同一张图上检测框位置偏移了几个像素,置信度也变了。

原因:ONNX 导出时如果开启了opset=12以下的算子,一些上采样操作可能被替换成精度较低的实现;另外,YOLOv8 的检测头输出是解码后的坐标,导出时忘记把nms=False的话,TensorRT 会帮你做一次额外的 NMS 过滤,导致框被吞掉。

解决:导出时固定输入尺寸,并禁用 NMS 层。用下面的命令导出,确保输出是原始的[1, 84, 8400]张量:

from ultralytics import YOLO model = YOLO("runs/tt100k/exp1/weights/best.pt") model.export( format="onnx", imgsz=1024, opset=12, dynamic=False, simplify=True, nms=False, # 必须关闭,留给 TensorRT 或自己处理 )

导出后用 ONNX Runtime 跑一次,将输出张量和 PyTorch 的推理结果做逐元素比对,最大误差不超过 1e-3 才说明导出干净。如果误差大,把simplify=False再试一次,某些简化算子会破坏模型精度。

6. 进阶:用注意力机制和模型蒸馏把模型压到能上路口

如果你已经完成了上述整个流程,下一步通常是把模型塞进边缘盒子——Jetson Nano、RK3588 或者一台工控机。交通标志检测的部署场景里,真正难的不是“跑起来”,而是“跑得又快又稳”。

我比较推荐的做法是给 YOLOv8 的 backbone 之后插入一个轻量注意力模块,然后用大模型蒸馏。以小模型为例,在ultralytics.nn.modules下面加一个 CBAM 模块,只需要改 forward 逻辑,训练时用固定的大模型作为教师,计算蒸馏 loss:

import torch import torch.nn as nn import torch.nn.functional as F def distillation_loss(student_feats, teacher_feats, temperature=4.0): # 特征图蒸馏,让学生的中间特征向教师靠拢 loss = 0 for s, t in zip(student_feats, teacher_feats): t_detached = t.detach() # 教师不反传梯度 s = F.normalize(s, dim=1) t_detached = F.normalize(t_detached, dim=1) loss += F.mse_loss(s, t_detached) return loss / len(student_feats) # 训练循环中叠加蒸馏损失 # total_loss = loss + 0.5 * distillation_loss(student_feats, teacher_feats)

这个技巧能让小模型在参数量减半的情况下,mAP 只掉 0.5~1 个点。关键参数有两个:温度temperature控制特征对齐的软硬程度,一般取 4~8;蒸馏权重0.5不能太大,否则学生会忽略原生标签,只学教师输出,反而过拟合到教师的错误上。我自己的习惯是前 30 个 epoch 只用原生标签训练,之后才叠加蒸馏 loss,让模型先把基本检测能力稳住。

另一个更实用的技巧是通道剪枝。YOLOv8s 的通道数本身不大,但依然能剪掉 30% 左右的冗余通道。用torch.prune按 BN 层的 gamma 值排序剪枝,剪完后微调 20 个 epoch。剪枝后的模型在 RK3588 上推理延迟能从 25ms 降到 15ms,精度损失可以控制在 2% 以内。注意剪枝必须在训练完成后再做,否则会直接破坏结构。

最后提醒一句:交通标志检测的落地效果,七成在数据,三成在模型。不要迷信“更好的网络结构”,先把自己的数据集清理干净、标签统一、类别均衡,比换任何骨干都有效。以上这些坑和技巧,都是我在实际项目里一个一个踩出来的,写出来希望能帮到你少走弯路。

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

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

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

立即咨询