YOLOv8交通标志检测实战:从数据训练到TensorRT/RKNN部署全解析
2026/9/20 10:46:33 网站建设 项目流程

1. 项目定位与整体设计思路

交通标志检测这几年在辅助驾驶、车路协同和城市交通管理里越来越刚需,尤其国内路况复杂,限速牌、禁行牌、警告牌种类多、样式杂,传统图像处理根本扛不住。我自己用YOLOv8把一整套交通标志检测系统从数据、训练、调优到部署完整跑了一遍,这里把过程、参数和踩过的坑一次性讲透,给准备拿YOLOv8做毕业设计或工程项目的朋友一个能直接照做的参考。

先说结论:YOLOv8做交通标志检测,核心优势不是某个单点指标多惊艳,而是整个链路非常顺——从数据标注格式、训练脚本、模型导出到多端部署,官方工具链覆盖得很全,省掉一大堆自己造轮子的时间。交通标志本身属于小目标为主、类别间外观高度相似的任务,这对YOLOv8的neck和head设计是个运气不错的匹配:它用anchor-free思路,对不同尺度目标的适应性比老版YOLOv5更平滑,少了很多anchor超参调优的麻烦。

1.1 为什么选YOLOv8而不是YOLOv5或其它模型

很多同学纠结YOLOv8和YOLOv5选哪个,我的判断很实际:如果目标是快速完成一个可演示、可扩展的交通标志检测系统,YOLOv8是性价比最高的选择。

YOLOv8相比v5有几处对交通标志检测很友好的改动:C2f模块替换了C3模块,梯度流通更顺,对小目标特征提取有帮助;head部分改成解耦头,分类和回归分支分开,收敛速度快,收敛后的精度也普遍比耦合头高一点;anchor-free机制让模型对标志这类尺度变化大的目标不再依赖精心设计的anchor参数。交通标志里既有30米外的小三角牌,也有近景里占画面一大半的大型指路牌,这种尺度跨度恰恰是anchor-free更容易cover的场景。

另一个现实原因:YOLOv8的生态太完整了。数据标注完转成txt格式,配置文件写好,一行命令就能训练,训练完能直接导出ONNX、TensorRT、RKNN等格式,从研究到部署的链路极度顺畅。不像部分检测框架,训练很强,部署时各种算子不支持,还得自己改代码。

1.2 交通标志检测的场景特点与难点

交通标志检测看起来简单,实际做起来坑不少。我总结了三个最典型的难点,这也是后面所有设计和调优方向围绕的核心。

一是小目标密度高。实拍道路画面里,很多标志在整张1920x1080图中只占二三十个像素。YOLOv8默认的检测头下采样倍数较大,对小目标的特征保留有限,所以后面会讲到要不要加P2层或改ASFF。

二是类别相似度高。限速40和限速60的牌子,形状、颜色几乎一样,只有数字不同;禁止驶入和禁止停车也是红圈加不同内部图案。这种细微差异要求模型学到足够细的纹理特征,稍不注意就会误检。

三是真实场景干扰多。逆光、雨雾、遮挡、运动模糊、树枝遮挡,都会让标志难以辨认。单靠模型硬扛不行,数据层面必须覆盖足够多的环境变化。

1.3 项目的整体技术路线

我这次做的是一个端到端系统,整体路线分四段:

  • 数据层:使用公开交通标志数据集 + 网上补充的真实场景图片,统一转成YOLO格式。
  • 训练层:基于YOLOv8s和YOLOv8n两个尺寸做对比实验,重点调优数据增强、冻结训练、损失超参。
  • 评估层:不只看mAP,还要统计不同尺寸目标、不同类别、不同光照条件下的单独表现,定位模型短板。
  • 部署层:先在PC端用PyTorch验证,再导出TensorRT引擎做C++推理,最后在边缘设备上跑RKNN量化模型。

下面每个环节单独展开,里面都有我实际跑通后的参数配置和排查经验。

2. 环境配置与数据准备

2.1 环境搭建:显卡不给力也能跑

先说我自己的机器配置:主力显卡是GTX 1660 Ti 6GB,放在今天算入门级。很多人问“GTX 1660 Ti能不能跑YOLOv8”,我的回答是:完全能跑,但你要学会跟显存打交道。6GB显存跑YOLOv8s、batch size设16、640分辨率在FP16下能跑,但想再提batch或加分辨率就很容易OOM。

我的建议环境版本组合如下,这套组合我已经稳定跑了大半年:

  • 系统:Ubuntu 20.04 / Windows 11都可以,建议Ubuntu,后续部署工具链更顺
  • Python:3.9或3.10
  • PyTorch:1.13或2.0以上,建议2.x,CUDNN和DDP的体验更稳
  • CUDA:11.8对应PyTorch 2.x最省心
  • 显卡驱动:525或更高系列
  • Ultralytics YOLOv8:直接从GitHub拉最新release版本,避免用pip旧版

安装命令其实很固定,但有个细节要注意,就是虚拟环境一定要独立建,不要直接装在系统Python里,否则后面装onnx、tensorrt这些依赖时很容易版本冲突。

conda create -n yolov8 python=3.9 -y conda activate yolov8 pip install ultralytics pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

安装完先跑一下官方预训练模型验证环境:

yolo predict model=yolov8s.pt source='https://ultralytics.com/images/bus.jpg'

能正常输出检测图,说明CUDA、PyTorch、Ultralytics三者通信正常。很多问题比如“装了半天一运行就报libcudnn错”,基本都是版本不对齐导致的,所以第一步把环境验证做扎实能省很多事。

2.2 交通标志数据集:公开数据集与自制数据怎么选

交通标志数据集我试过几个,最终根据场景做了混合。

  • CCTSDB:长沙理工大学发布的交通标志数据集,场景多为国内道路,类别较粗(指示、警告、禁令三类),适合做基础训练。
  • TT100K:腾讯发布的数据集,包含上万张真实街景图和几十万个标注框,类别细,适合细分类任务,但需要自己重新整理成YOLO格式。
  • GTSRB:德国交通标志数据集,偏分类,但如果要做迁移学习或额外的分类验证,可以拿来当辅助。

我的建议是:如果做毕设或演示系统,直接选CCTSDB或TT100K精标一部分数据就够;如果做实际落地,一定要补充自己场景的图片。原因很简单:公开数据集里的标志干干净净,而实际道路中的标志可能旧、歪、脏、被贴小广告,泛化差距很大。

整理数据的时候注意几个点:

  • 图片不要直接缩放到640,要保持原始宽高比,YOLO训练时内部会做letterbox,不会变形。
  • 剔掉漏标、错标的图片,一张坏标注给模型带来的伤害比少一张图更大。
  • 镜像翻转对交通标志要慎重。限速、禁行等符号镜像后也有实际语义,但部分文字型标志镜像后语义会变,所以随机翻转概率我调得很低,只保留左右翻转,概率0.2左右。

2.3 标注格式转换与数据集划分

我现在统一用YOLO格式训练,标注文件是一个txt,每行是“类别 x_center y_center width height”,四个坐标都是归一化到0~1的相对值。如果你用的是LabelImg、Labelme或X-AnyLabeling,导出时注意坐标格式转换。

这里给一个我自己用的转换脚本思路,把VOC XML或COCO JSON转成YOLO txt。VOC转YOLO的核心逻辑很简单,就是读取XML里的bndbox坐标,然后除以图像宽高,并换算成中心点坐标:

import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_file, out_txt, class_names): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text cls_id = class_names.index(cls_name) box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') with open(out_txt, 'w') as f: f.write('\n'.join(lines))

数据集划分上,我一般按8:1:1分成训练、验证、测试。但要注意,交通标志数据经常出现同一地点连拍的多张图,如果随机划分,训练集和验证集里可能混入同一标志的不同帧,导致指标虚高。更严格的做法是按场景或按视频序列划分,确保验证集能反映真实泛化能力。尤其是毕设答辩或论文写实验时,验证集划分方式一定要写清楚,这是审稿人容易追问的点。

3. 模型训练全流程

3.1 数据集配置文件与基础超参数设置

训练前要写一个dataset.yaml文件,格式非常固定:

path: /data/traffic_sign train: images/train val: images/val test: images/test names: 0: speed_limit 1: no_entry 2: warning 3: guide ...

注意path字段最好写绝对路径,相对路径有时候在导出或二次训练时会因为工作目录切换找不到数据。names的顺序必须和标注文件里的类别ID严格一致,哪怕只是调换顺序,训练出来的模型推理时类别就全错了。

训练基础命令如下:

yolo detect train \ data=traffic_sign.yaml \ model=yolov8s.yaml \ pretrained=yolov8s.pt \ epochs=120 \ batch=16 \ imgsz=640 \ patience=20 \ optimizer='SGD' \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=3 \ seed=42

这里重点说一下几个参数选择的原因。

batch size在6GB显存卡上,yolov8s+640分辨率+FP16最多跑到16左右。如果OOM,优先降batch而不是降分辨率,因为分辨率对检测精度的损失更明显。如果batch实在上不去,可以用梯度累积等效增大batch,YOLOv8里通过batch=-1自动检测,但我更习惯手动设一个稳定值。

优化器我选SGD而不是AdamW。虽然AdamW收敛快,但SGD在目标检测上最终精度往往更高,尤其当训练轮数拉长时,SGD的泛化优势会体现出来。在交通标志这种相对简单的任务上,SGD训练100轮左右不会浪费时间。

3.2 冻结训练:小数据集的关键技巧

很多人在小数据集上直接全量微调,结果过拟合得一塌糊涂,loss降不下去,val指标也不涨。我推荐的做法是先冻结骨干网络训练,再解冻微调。

什么叫“冻结”?就是把模型某一层的参数固定住,训练时不更新这些权重。用一个生活类比:你让一个已经熟读交通法规的老司机去学一个新城市的开车习惯,不用从头教他怎么看红绿灯、怎么踩刹车,只需要让他适应新城市的特殊路况就够了。冻结backbone就是让预训练模型保留基础视觉能力,只训练检测头去适应当前任务。

YOLOv8的冻结训练很简单:

yolo detect train \ data=traffic_sign.yaml \ model=yolov8s.yaml \ pretrained=yolov8s.pt \ freeze=10 \ epochs=50 \ batch=16 \ imgsz=640

freeze=10表示冻结前10层,对YOLOv8来说基本就是整个backbone。我的经验是:第一轮先冻结backbone训50轮,让head收敛到能用的状态;然后第二轮解冻全部层,用较小的学习率再训50~70轮。这样训练比一上来就全量训练稳定很多,尤其在CCTSDB这种几千张图的小数据集上,效果差距非常明显。

实际操作中,解冻后的学习率一般降到冻结阶段的五分之一到十分之一,我通常设为0.001左右,避免破坏已经收敛的头部参数。

3.3 损失函数曲线怎么看、怎么画

训练完成后,Ultralytics会自动生成results.png和results.csv,里面已经包含了train/box_loss、train/cls_loss、train/dfl_loss以及对应的val损失曲线。但很多人只会看趋势,不会定性地判断模型是否正常。

我的判断标准很明确:

  • train和val的loss都持续下降并趋于平稳,说明模型正常收敛。
  • train loss一直降、val loss降到某个点开始反弹,说明过拟合,此时要看早停或加大数据增强。
  • train和val loss都降不动,而且一直在高位抖动,可能是学习率太大或标注噪声太高。
  • val loss比train loss低很多,反而要警惕,通常是验证集分布和训练集太接近,或者验证集太小,模型在这个验证集上没有区分度。

有时候我需要把某一轮的训练曲线重新画一遍,比如论文里需要更美观的图,或者只看某一项loss时Ultralytics的默认图太笼统。这时候直接用pandas和matplotlib读results.csv画图即可:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/detect/train/results.csv') # 去掉列名首尾空格 df.columns = df.columns.str.strip() plt.figure(figsize=(10, 6)) plt.plot(df['epoch'], df['train/box_loss'], label='train box_loss') plt.plot(df['epoch'], df['val/box_loss'], label='val box_loss') plt.plot(df['epoch'], df['train/cls_loss'], label='train cls_loss') plt.plot(df['epoch'], df['val/cls_loss'], label='val cls_loss') plt.xlabel('epoch') plt.ylabel('loss') plt.legend() plt.grid(True) plt.savefig('custom_loss_curve.png', dpi=200)

画出来的曲线注意观察有没有“台阶式”突变,如果有,很可能是lr scheduler调整阶段(比如warmup结束或cosine decay的某个点),一般不是异常。

3.4 训练结果评估:mAP之外还要看什么

训练结束通常看mAP50和mAP50-95。mAP50是IoU阈值0.5时的平均精度,更关注“框大概准不准”;mAP50-95是多个IoU阈值的平均,更关注“框的精确定位能力”。交通标志检测任务中,mAP50-95尤其重要,因为辅助驾驶要求框位足够精确,不能只是一个大致区域。

但只看mAP不够,我用Ultralytics生成的混淆矩阵和P/R曲线做进一步分析,重点检查:

  • 哪些类别之间经常互相混淆。比如限速40和限速60的混淆往往是因为训练数据里这两类数量不平衡。
  • 哪些类别的recall低。recall低意味着漏检多,可能是小目标样本少,需要在数据增强里加强mosaic和copy-paste。
  • 模型在不同尺度下的表现。YOLOv8默认输出三种尺度特征图,如果小目标表现差,可以考虑增加P2输出层。

Epoch和验证集的判断上,我还会打印一个pr_curve.png来看看当前置信度阈值下的最优工作点。部署时候用哪个confidence阈值,很多人的习惯是直接用默认0.25,但实际应该根据pr曲线来选——如果追求高召回(比如漏检一个限速牌很严重),就降低conf到0.1甚至0.05;如果追求低误报,就提高到0.4甚至0.5。

4. 系统落地与推理部署

4.1 PC端实时检测流程与参数选择

训练完得到best.pt后,我先在PC端搭了一个简单的实时检测脚本,用OpenCV读摄像头或视频流,然后传给YOLO模型做推理。

import cv2 from ultralytics import YOLO model = YOLO('best.pt') cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break results = model.predict(frame, conf=0.35, iou=0.5, imgsz=640, device=0) annotated = results[0].plot() cv2.imshow('traffic_sign_detection', annotated) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这里要注意model.predict如果放在循环里,每次都会执行预处理器、推理器和后处理器,性能不如用model(frame)直接调用高。实际项目中尽量在循环外加载模型,循环内只传frame数组。另一个性能点是imgsz的选择,640是最常用的平衡点,在GTX 1660 Ti上yolov8s单帧推理时间大约在40~60ms,如果嫌慢可以降到480,对交通标志这种相对大一些的目标影响不大,FPS能明显提升。

我实测下来,GTX 1660 Ti跑yolov8s在640分辨率下FP32推理大概20~25 FPS,用TensorRT FP16可以跑到50~60 FPS,差距很大。所以如果对实时性有要求,PC端也务必上TensorRT。

4.2 TensorRT 8.6加速与C++部署要点

TensorRT部署的第一步是把PyTorch模型转成ONNX,这一步的坑最多。直接给出可用的导出命令:

yolo export model=best.pt format=onnx opset=12 simplify=True dynamic=False imgsz=640

opset建议12,太高或太低都可能导致TensorRT某些算子不兼容。simplify用True,会调用onnx-simplifier去优化计算图。dynamic参数我建议设False,固定输入尺寸640x640,TensorRT的优化效果最好;如果你需要动态分辨率,后面部署复杂度会明显上升,非必要不搞。

然后用trtexec生成engine文件:

/usr/src/tensorrt/bin/trtexec \ --onnx=best.onnx \ --saveEngine=best_fp16.engine \ --fp16 \ --workspace=4096

6GB显存的老显卡用4GB workspace就行。生成过程中如果看到某一层op不支持FP16,TensorRT会自动fallback到FP32,不影响最终运行,只是性能可能达不到预期。

C++部署的核心流程是:读engine → 创建context → 申请GPU显存和CPU内存 → 预处理(letterbox + BGR转RGB + 归一化) → 推理 → 后处理(解码+过滤+NMS)。这里最容易出问题的是输入数据的排布,YOLOv8的输入是NCHW格式,也就是batch、通道、高、宽,如果你在C++里用OpenCV读到的图像是HWC格式,直接丢给模型会全乱,必须先做transpose。

另一个容易踩坑的是后处理。YOLOv8的输出不是一个直接的检测框列表,而是特征图解码后的高维数组,需要做解码,把anchor-based时代的网格解码逻辑去掉,直接根据4个bbox回归量计算坐标。如果用ONNX导出的模型,输出尺寸是[1, 84, 8400](假设80类),这个8400是三种尺度特征图所有候选框的总数,每个候选框对应“x_center、y_center、width、height、各类别概率”。解码后再做置信度过滤和NMS。

我刚开始部署时一直检测不到任何目标,排查半天发现是输出维度解析错了,把这个维度对不上导致所有结果都被丢掉。建议先用Python的onnxruntime或TensorRT Python接口验证一遍ONNX输出,确认维度后再写C++。

4.3 边缘设备RK3588部署心得

把模型在边缘设备上跑是项目进阶的关键一步,很多同学毕设做完PC端感觉不够“硬核”,就会尝试在开发板上部署。我调研之后选了RK3588,因为它的NPU算力在同类开发板里比较突出,6 TOPS的INT8算力跑YOLOv8s能到30 FPS以上,而且开发工具链相对成熟。

部署流程不复杂,但步骤比较琐碎:

  1. 导出ONNX模型,和之前TensorRT用的是一样的操作,但注意RKNN-Toolkit对某些算子的支持有差异,我建议导出ONNX后用工具链官网对比一下算子支持情况。
  2. 用RKNN-Toolkit2加载ONNX模型,配置量化方式(通常选INT8),提供校准数据集(几百张典型图片即可,不需要带标签)。
  3. 转换成RKNN格式,部署到开发板上运行。

RK3588上最常遇到的问题有两个。一是量化后精度掉得厉害,尤其是小目标检测。解决思路是先用足够有代表性的校准数据集,数量500张起步,内容要覆盖白天、夜晚、雨雾等场景,不能让校准集太单一。二是一些YOLOv8特有的算子在NPU上不支持,比如DFL(Distribution Focal Loss)解码部分,有时需要用软硬件协同的方式改后处理,或者在模型导出时做一些结构替换,让NPU支持的算子去完成。

我实际踩过的一次坑是,转成RKNN后模型能跑,但检测框全部偏移,最后发现是因为ONNX里的输出层没有去掉最后的sigmoid操作,而RKNN工具链又不会再自动做一遍,导致输出概率值域不对。说到底,部署问题七成出在格式转换和前后处理上,模型本身很少需要改。

4.4 常见问题与排查技巧实录

我把训练和部署过程中遇到的高频问题整理成了一张速查表,都是实际能落地的排查思路。

现象可能原因解决办法
训练时CUDA OOMbatch过大或分辨率过高降batch到8或4,开FP16,使用梯度累积
train loss不降学习率过高或标注噪声大降低lr0到0.001,检查标注框是否越界
val loss反弹过拟合增大mosaic、hsv增强概率,或提前早停
限速牌类别互相混淆类别间样本不均衡增加少类别的复制增强,或用类别权重
小目标漏检多特征图对小目标不敏感增加P2检测层,或使用更大分辨率训练
ONNX导出报错opset版本不匹配统一opset=12,打开simplify
TensorRT推理结果错乱输入数据排布不对核对CHW转换和归一化参数
RKNN INT8掉点严重校准集不具代表性增加校准图片数量和场景多样性
摄像头实时检测卡顿后处理在CPU耗时太高把预处理和后处理也放到GPU,或降到480分辨率

另外分享一个独家技巧:交通标志检测模型的置信度阈值不要一概而论。PC端演示时conf设0.4问题不大,但在边缘设备或雨天场景下,建议把conf降到0.25左右。因为部署环境的图像质量和训练集差异较大,宁可多几个误检,也别漏掉真正重要的限速牌。

最后再分享一个我在实际项目中反复验证的经验:训练阶段和部署阶段的图像预处理必须一致。很多人训练时用Ultralytics默认的letterbox方式填充灰色,但自己写部署代码时却直接拉伸图片,导致检测精度下降严重。我自己是把预处理函数抽成单独模块,训练和部署共用同一份代码,这个看似不起眼的细节,往往比你换模型、调超参数还管用。

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

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

立即咨询