刚做完一个基于YOLOv8的交通标志检测项目,从数据准备到模型落地部署走了个完整的流程。这中间踩了不少坑,也积累了一些实战经验。很多朋友问我YOLOv8怎么做交通标志检测,正好借这个机会把整个项目复盘一遍,从环境搭建、数据集处理、模型训练,到后面的TensorRT部署、RK3588板端移植,整个过程完整记录,希望给正在做类似项目或者准备拿YOLOv8做毕业设计的同学一些参考。
实际上,交通标志检测这个任务看起来简单,真正做起来有很多细节。交通标志本身是小目标,在画面中占的比例很小,再加上户外光照变化、遮挡、运动模糊这些因素,模型想要稳定识别并不容易。YOLOv8作为目前主流的检测模型,在精度和速度上取得了不错的平衡,而且官方提供的接口非常友好,不管是训练自己的数据集还是后续部署,都有比较成熟的方案。
1. 项目整体设计与思路拆解
1.1 为什么选YOLOv8做交通标志检测
先说结论:YOLOv8在交通标志检测这个任务上,属于综合性价比最高的方案之一。
对比之前的YOLOv5,YOLOv8在结构上做了不少调整。最核心的变化是换掉了Anchor-Based的方式,改成了Anchor-Free,这直接省掉了聚类锚框和调anchor的麻烦。对于交通标志这种尺寸比较固定的目标来说,Anchor-Free的解码方式反而让边框预测更稳定。其次是backbone里的C3模块换成了C2f模块,梯度流更丰富,特征提取能力有提升。Head部分也改成了解耦头,分类和回归分成两个分支,收敛速度和精度都有改善。
从工程角度看,YOLOv8官方仓库的文档齐全,训练接口经过封装,调用起来非常简单。不需要自己写训练循环,也不需要手动处理数据增强、学习率调度这些细节,对于做应用落地的开发者来说非常友好。
交通标志检测的场景有两个显著特点。第一是小目标问题,一张1920x1080的路面图像里,远处的限速标志可能只有30x30像素,对特征提取的要求很高。第二是类别不平衡,有些标志(比如限速40、限速60)出现频率高,有些警告标志则很少见。YOLOv8在数据增强和多尺度训练上的支持比较完善,这两类问题都有办法缓解。
1.2 系统整体架构与工作流程
整个系统分成四个层次:数据层、训练层、评估层、部署层。
数据层主要负责数据集的收集、整理、标注格式转换和数据增强策略。训练层包括模型选型、超参数配置、训练过程监控。评估层是在验证集上计算mAP、Precision、Recall等指标,并可视化检测效果。部署层是把训练好的模型导出为不同格式,比如ONNX、TensorRT engine,或者通过RKNN工具链移植到RK3588等边缘设备上。
这四个层次是串行依赖的关系,前面环节出了问题,后面全白做。我在实际项目里最深的体会是:数据准备阶段花的时间往往会超过训练和调参的时间,这个环节非常值得投入精力。
1.3 硬件选型的现实考量
训练阶段的硬件直接决定了迭代速度。我手头有RTX 5060和GTX 1660 Ti两种显卡,做了对比测试。
RTX 5060的显存和算力都比较充裕,用YOLOv8n模型、batch size 32的情况下,1920分辨率输入一轮迭代大概1.2秒左右。GTX 1660 Ti显存只有6GB,batch size超过16就会OOM,只能降低到8或者16,同时输入分辨率要降到1280,一轮要跑3秒多。如果预算允许,建议至少用12GB显存的显卡,训练体验会好很多。
不过即便只有GTX 1660 Ti这样的显卡,YOLOv8n和YOLOv8s这两个小模型还是可以跑的,只是需要把batch size调小。博主“白老师”在视频里也提到了GTX 1660 Ti跑YOLOv8的配置技巧,核心思路就是优先保证输入分辨率,其次再考虑batch size,因为分辨率对检测精度的影响远大于batch size。
2. 环境配置与数据准备
2.1 环境搭建完整步骤
这是整个项目中最容易翻车的地方。YOLOv8的安装虽然官方文档写得很简单,但真正跑起来的时候,PyTorch版本、CUDA版本、cuDNN版本之间的匹配问题经常让人头大。
我这里的推荐配置方案如下:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| Python | 3.8 - 3.10 | 3.10以上部分依赖库可能不兼容 |
| CUDA | 11.8 或 12.1 | 根据显卡驱动版本选择 |
| cuDNN | 对应CUDA版本的匹配版本 | 最好用8.6以上 |
| PyTorch | 2.0.0 - 2.1.1 | 实测这两个版本最稳定 |
| ultralytics | 8.0.x 以上 | 版本更新很快,锁定大版本即可 |
我先装CUDA和cuDNN,然后创建Conda虚拟环境,最后装PyTorch和ultralytics包。具体命令如下:
# 创建虚拟环境 conda create -n yolo python=3.9 conda activate yolo # 安装PyTorch(CUDA 11.8版本) pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 安装ultralytics pip install ultralytics # 测试安装 python -c "from ultralytics import YOLO; print('OK')"这里有几个细节要提醒:
注意PyTorch的安装源,不要用默认的PyPI源装PyTorch,那样装到的多半是CPU版本,训练速度慢到怀疑人生。必须到CUDA对应的index-url安装GPU版本。
安装完成之后,用nvidia-smi确认显卡能识别,再在Python中输入import torch; print(torch.cuda.is_available())确认PyTorch能看到显卡。如果是True,说明环境没问题。
2.2 交通标志数据集的获取与预处理
交通标志检测没有统一的公开数据集标准,最常见的选择是TT100K和CCTSDB。TT100K是中国交通标志数据集,包含上万张实景图像,覆盖了中国路面上绝大多数标志类型。CCTSDB是长沙理工大学开源的交通标志数据集,规模相对小一些。如果做国际通用场景,GTSRB也可以考虑,但它是单帧分类为主,检测场景适配度不如前两者。
我这次用的是TT100K的子集,选了场景覆盖比较全的类别,大概50类左右。TT100K原始数据是JSON格式标注(COCO风格的框),需要转成YOLO格式的txt文件。YOLO格式的中心点坐标和宽高都是相对于图像宽高的归一化浮点数。转换脚本可以直接自己写:
import json import os def convert_tt100k_to_yolo(json_path, save_dir, img_width, img_height): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) for annotation in data['annotations']: image_id = annotation['image_id'] category_id = annotation['category_id'] bbox = annotation['bbox'] # [x, y, width, height] x_center = (bbox[0] + bbox[2] / 2) / img_width y_center = (bbox[1] + bbox[3] / 2) / img_height width = bbox[2] / img_width height = bbox[3] / img_height txt_path = os.path.join(save_dir, f"{image_id}.txt") with open(txt_path, 'a') as f_txt: f_txt.write(f"{category_id} {x_center} {y_center} {width} {height}\n")标注文件格式转换完成之后,数据集目录按照YOLO的标准结构组织:
datasets/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml里要指定路径和类别信息:
train: datasets/images/train val: datasets/images/val test: datasets/images/test nc: 50 names: ['限速40', '限速60', '限速80', '解除限速', '禁止左转', ...]类别名称建议直接用中文可读性更好,实际训练时内部用的是索引编号,影响不大。
2.3 数据增强策略的取舍
交通标志数据增强的核心原则是:适度增强,不要过度。经过实测,这里有几个经验:
Mosaic增强在目标检测中泛化提升明显,但交通标志尺寸较小,Mosaic把四张图拼接后小目标进一步缩小,容易导致训练困难。建议在训练后期关闭Mosaic(close_mosaic参数设为一个较小的值,比如5个epoch)。
hsv_h、hsv_s、hsv_v这三个颜色增强对交通标志比较友好,因为实际场景中光照变化大,适当调整色相和饱和度可以提升模型在不同光线下的鲁棒性。平移和缩放增强会导致部分标志被裁剪,不宜设置过大,scale控制在0.3-0.5之间比较合适。翻转增强对交通标志要慎重,左转和右转箭头在翻转后会语义反转,如果数据集中包含方向性标志,不要开左右翻转,只开上下翻转(如果有倒装标志的话)。
3. 模型训练与核心参数调优
3.1 训练参数配置详解
YOLOv8的训练入口非常简单,一行命令的事情:
yolo train model=yolov8n.yaml data=datasets/data.yaml epochs=200 imgsz=640 batch=16 device=0但参数设置直接影响训练效果。我把关键参数整理成一张表:
| 参数 | 建议值 | 说明 |
|---|---|---|
| model | yolov8n / yolov8s | 先小模型跑通,再根据效果升级 |
| epochs | 150-300 | 数据集小可以多跑一些 |
| imgsz | 640 或 1280 | 显存够用就尽量用1280,对小目标友好 |
| batch | 显存允许的最大值 | 16-64之间 |
| freeze | 前10-20个epoch冻结backbone | 加快收敛,防止预训练权重被破坏 |
| optimizer | AdamW 或 SGD | 小数据集用AdamW,大一点用SGD |
| lr0 | 0.01(SGD)/ 0.001(AdamW) | 初始学习率 |
| close_mosaic | 训练最后10个epoch设为0 | 避免小目标丢失 |
| patience | 30-50 | 早停耐心值 |
最容易被忽视的参数是imgsz。YOLOv8默认的640分辨率在COCO上表现不错,但交通标志多为小目标,640分辨率下远处的标志可能只有几个像素。有条件的情况下把输入分辨率提升到1280,对小目标检测效果提升非常明显。代价是显存占用成倍增加、训练时间变长。折中方案是先以640跑通流程,后续再用1280微调。
freeze参数的应用要灵活。小数据集上预训练权重本身已经在COCO上学到了通用的特征,如果一开始就全部解冻微调,很容易破坏底层特征。实际操作中我习惯先冻结backbone训练10个epoch,等损失稳定后再解冻全部层进行完整微调。如果是只针对自己的场景微调,或者数据量非常小,可以先用freeze=10跑一部分,再重新加载权重全量训练。
3.2 损失函数曲线观察与分析
训练过程中监控loss曲线的走势是判断模型是否正常收敛的关键手段。YOLOv8训练完成后会在runs/detect/train目录下生成results.csv文件,里面包含了每一轮的box_loss、cls_loss、dfl_loss以及对应的p、r、mAP50、mAP50-95等指标。
我自己写了一个简单的脚本,从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() # 创建一个2x3的子图 fig, axes = plt.subplots(2, 3, figsize=(18, 10)) # 各种损失 loss_metrics = [ ('train/box_loss', 'Box Loss'), ('train/cls_loss', 'Cls Loss'), ('train/dfl_loss', 'DFL Loss'), ('val/box_loss', 'Val Box Loss'), ('val/cls_loss', 'Val Cls Loss'), ('val/dfl_loss', 'Val DFL Loss') ] for idx, (col, title) in enumerate(loss_metrics): ax = axes[idx // 3][idx % 3] x = df['epoch'] y = df[col] ax.plot(x, y, linewidth=2) ax.set_title(title) ax.set_xlabel('Epoch') ax.grid(True) plt.tight_layout() plt.savefig('loss_curves.png', dpi=150)正常训练过程中,train loss和val loss应该同步下降,并且两条曲线之间的gap不会太大。如果train loss持续下降但val loss在某个epoch后开始回升,说明过拟合了,可以调大weight_decay或者提前停止。如果两条曲线都下降得很慢,可能学习率太低了。如果出现了明显的震荡和脉冲,常用手段是降低learning rate或者检查数据标注错误。
3.3 不同硬件条件下的训练策略
训练免不了等待。GPU越强迭代越快,除此之外还有两个策略可以明显提速:
混合精度训练,默认开启,能减少约40%的显存占用,训练速度提升20-30%。
多GPU训练,一张卡不够时,YOLOv8直接支持多卡并行:
yolo train model=yolov8n.yaml data=datasets/data.yaml epochs=200 imgsz=640 batch=64 device=0,1batch参数会按卡数均分,比如指定batch=64、device=0,1,就是每张卡32。多卡训练要注意batch size不要减小到影响BN统计的程度,一般单卡batch最好大于8。
如果用的是一台苹果电脑,MPS加速在YOLOv8里也可以直接用,device=mps即可。实测M2 Max跑YOLOv8s大概比CPU快5-8倍,但相比NVIDIA GPU还是有差距。
4. 模型改进与轻量化实践
4.1 针对交通标志的模型改进思路
YOLOv8原版模型在通用目标检测上表现均衡,在特定场景下仍然有提升空间。交通标志检测最常见的改进方向有三个:
注意力机制,在Head或Backbone中加CBAM、SE或CA模块,能让模型更关注标志区域。我试过在C2f中引入CA注意力,对小目标召回率的提升大约有1-2个点mAP。
特征融合改进,YOLOv8的PANet结构虽然已经很好,但对小目标仍然不够敏感。引入ASFF(自适应空间特征融合)可以自动学习不同层级特征的权重,对小目标检测有正向效果。不过ASFF实现复杂度高一些,需要修改网络结构代码,对于常规需求来说不是必需的。
Head改进,比如把检测头换成DynamicHead或者增加一个小目标检测层(P2层)。对于交通标志这种密集小目标场景,P2层可以在原图分辨率1/4的位置增加一个检测尺度,对小目标出框能力提升比较明显,代价是计算量增加不少。
要注意的是,改进网络结构之后需要把改进后的结构写成一个新的yaml文件,并且在训练时指定这个yaml作为模型定义。不要直接在别人的代码上乱改,否则很难排查问题。
4.2 轻量化模型与嵌入式部署场景
交通标志检测系统经常要部署在嵌入式设备上,比如路侧单元、车载终端、移动设备。这时候模型大小和推理速度比精度更重要。
YOLOv8官方提供的n、s、m、l、x五个规模中,n和s最适合嵌入式场景。YOLOv8n的参数量只有约300万,模型文件大小约6MB,在RK3588上推理速度可以做到30ms以内。代价是精度相比YOLOv8s低一些,mAP50大概差2-3个点。
如果要进一步压缩模型,可以选择的技术路线包括:
通道剪枝,对训练好的模型做结构化剪枝,删除影响较小的通道,压缩率可以达到30%-50%。TensorRT和ONNXRuntime都支持部分剪枝后的加速优化。知识蒸馏,用一个大的YOLOv8l模型作为教师网络,蒸馏一个YOLOv8n学生网络,学生模型的精度可以提升1-2个点。轻量化Backbone替换,比如把Backbone换成MobileNetV4或ShuffleNetV2,计算量可以再降一个量级,但需要自己修改网络定义并适配预训练权重。
我在轻量化上的实操经验是:优先尝试YOLOv8n原版加TensorRT FP16量化,这个组合不需要改任何网络结构,精度损失很小,推理速度已经足够快。如果还是不够,再做剪枝或者Backbone替换,逐级往下走。
4.3 评估指标的选取与模型选择
交通标志检测最终效果到底怎么评价,不能只看mAP。我习惯组合看几个指标:
| 指标 | 关注点 | 组合使用场景 |
|---|---|---|
| mAP@0.5 | 定位宽松场景下的整体精度 | 与mAP@0.5:0.95一起评估 |
| mAP@0.5:0.95 | 定位严格场景下的精度,对边框回归更敏感 | 模型间横向对比 |
| Precision | 检测结果中正确比例,误检敏感 | 防止路口标志误识别成限速标志 |
| Recall | 漏检比例,对漏检敏感 | 小目标场景 |
| F1 Score | 平衡Precision和Recall | 单模型快速评估 |
对于交通标志检测系统来说,Recall和F1往往比mAP更关键。实际路测中错过一个限速标志比多报一个标志后果严重得多。所以如果两个模型mAP相近,我会选Recall更高的那个,宁可多一些误检,也不能漏掉关键标志。
5. 模型部署:从Python原型到正式落地
5.1 ONNX导出与TensorRT 8.6部署
训练好的模型要落到实际系统,通常不能直接跑Python。典型路径是先转ONNX,再转TensorRT engine,然后C++调用。
先用ultralytics的导出接口生成ONNX:
yolo export model=best.pt format=onnx opset=12 imgsz=640 simplify=True导出ONNX后会生成一个与模型同名的.onnx文件。这个环节有几点需要注意:
simplify=True会做图优化。opset=12在TensorRT 8.6上兼容性最好。imgsz要和训练时保持一致,或者至少是倍数关系。
接下来用TensorRT生成推理引擎。TensorRT 8.6版本配合对应的CUDA环境:
trtexec --onnx=best.onnx --saveEngine=best.trt --fp16如果希望动态输入尺寸,加--minShapes=input:1x3x640x640 --optShapes=input:1x3x640x640 --maxShapes=input:1x3x640x640参数。固定尺寸的engine在推理时会更快,动态尺寸灵活一些,可以根据实际场景取舍。
C++上TensorRT推理的核心步骤包括:读取engine文件、创建runtime和context、分配GPU显存、执行推理。一个最简版本的推理代码框架是:
#include <NvInfer.h> #include <fstream> #include <vector> class TRTInference { public: void loadEngine(const std::string& enginePath) { std::ifstream file(enginePath, std::ios::binary); std::vector<char> data(std::istreambuf_iterator<char>(file), {}); runtime = nvinfer1::createInferRuntime(logger); engine = runtime->deserializeCudaEngine(data.data(), data.size()); context = engine->createExecutionContext(); } void infer(float* input, float* output) { // 分配CUDA内存、复制输入数据、执行推理、复制输出结果 // 这里是标准的CUDA流程 } private: nvinfer1::IRuntime* runtime; nvinfer1::ICudaEngine* engine; nvinfer1::IExecutionContext* context; };TensorRT的部署门槛主要在环境配置上。CUDA版本和TensorRT版本必须匹配,我踩过的坑包括:TensorRT 9.0对opset更高的ONNX模型可能报不支持的错误,TensorRT 8.6和8.5的engine文件不兼容,需要重新序列化。强烈建议用Docker镜像来固定环境,避免部署到客户机器上后环境不一致的问题。
5.2 RK3588边缘设备部署全流程
RK3588是瑞芯微的一款高性能边缘AI芯片,有6 TOPS的NPU算力,在嵌入式场景下运行YOLOv8n非常合适。很多同学问怎么在RK3588上跑YOLOv8,这里把完整流程梳理一遍。
第一步,RK3588不支持直接跑TensorRT,需要用瑞芯微自带的RKNN-Toolkit2工具链将ONNX模型转换为RKNN格式,然后在板端通过RKNN Runtime进行推理。
转换流程大致是:
# 在PC上安装RKNN-Toolkit2 pip install rknn-toolkit2 # 编写转换脚本 from rknn.api import RKNN rknn = RKNN() # 配置量化参数 rknn.config(mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588') # 加载ONNX模型 ret = rknn.load_onnx(model='best.onnx') # 构建RKNN模型 ret = rknn.build(do_quantization=True, dataset='dataset.txt') # 导出RKNN文件 ret = rknn.export_rknn('best.rknn')转换过程最关键在于do_quantization=True的量化设置。RKNN工具链默认以INT8量化,如果校准数据集选取得不好,量化后精度损失会很明显。我通常的做法是从训练集里随机抽200-300张有代表性的图片生成一个dataset.txt,量化后的mAP下降控制在1-2个点以内。
第二步,在RK3588板子上部署。板端推理用的是RKNN Runtime的C API或Python API。Python API更适合快速验证:
from rknnlite.api import RKNNLite # 初始化 rknn_lite = RKNNLite() # 加载模型 rknn_lite.load_rknn('best.rknn') rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2) # 推理 outputs = rknn_lite.inference(inputs=[img])板端推理的速度受NPU核心分配的影响很大。RK3588有3个NPU核心,可以单独使用也可以组合使用。实测YOLOv8n在RK3588上单核推理约30ms,三核并行可以到15ms左右。功耗比运行TensorRT的GPU方案低得多,非常适合做边缘部署。
正点原子有一套完整的RK3588部署YOLOv8的教程,核心步骤跟我上面写的类似,但在模型预处理和输出后处理上有很多细节需要注意。YOLOv8的输出解码在板端需要自己写,包括通过conf阈值过滤、NMS等。如果对C++更熟悉,可以在板端用C++版本的RKNN API做推理,性能相对Python版本还会有一点提升。
5.3 针对交通标志检测场景的部署优化
交通标志检测部署还有一个特殊需求:实时视频流里的连续检测。路面上的标志牌是静止的,但摄像头在移动,同一块标志会出现在连续多帧画面中。如果每一帧都重新检测,计算量大且容易出现检测结果跳动。
常见的优化方案是加一个跟踪器,比如ByteTrack或DeepSORT,把连续帧中的检测框关联起来。这样既减少了重复检测的算力浪费,又可以通过多帧投票机制过滤掉单帧误检。我在项目中采用的是YOLOv8检测加ByteTrack跟踪的组合,在RK3588上整体帧率仍能保持25fps以上,检测稳定性明显比逐帧独立检测要好。
6. 常见问题与排查技巧实录
6.1 训练阶段高频问题速查
整个项目做完,整理了这份问题速查表,覆盖了训练和部署阶段最常见的故障。每一条都是我实际遇到或者给网友排查过的,应该能帮大家少走很多弯路。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练时OOM,显存不足 | batch size过大或imgsz过大 | 减小batch,或降低imgsz,或换更小的模型版本 |
| 训练一两轮后loss为nan | 学习率过高或数据里有异常标注 | 降低lr0,检查数据集的bbox是否越界、标签是否超出nc范围 |
| 验证集mAP一直很低(<0.3) | 数据标注错误、类别不平衡、imgsz过小 | 优先检查标注文件和类别对应关系,换大输入分辨率 |
| 模型预测框位置偏移明显 | Anchor-Free解码后处理没匹配 | 检查导出的ONNX输出的shape,确认用对输出分支 |
| 导出ONNX到TensorRT报错 | 算子不支持或opset版本不兼容 | 用opset=12重新导出,检查模型中是否有自定义层 |
| RKNN转换后精度骤降 | 量化校准集不具代表性 | 从训练集均匀抽取校准图片,增加校准集数量到300张以上 |
这里我要特别展开说一下loss为nan的问题。很多人上来就怀疑是网络结构坏了,实际上90%的情况是数据集标注出了问题。YOLO格式的标注里,如果出现坐标大于1或者width/height为0的情况,训练过程中计算损失就会产生无效值。我自己后来在数据预处理里加了一个校验函数,专门检查标注文件的范围:
def check_labels(label_path): import numpy as np with open(label_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: return False cls_id = int(parts[0]) x_center = float(parts[1]) y_center = float(parts[2]) w = float(parts[3]) h = float(parts[4]) # 检查坐标范围 if not (0 <= x_center <= 1 and 0 <= y_center <= 1): return False if not (0 < w <= 1 and 0 < h <= 1): return False return True6.2 训练曲线异常的诊断经验
Loss曲线的形态基本能反映训练的健康状况。我在多轮调试中有几个总结性的判断方法:
如果train loss不断下降,但val loss在第30轮左右开始上升,这是过拟合的标志。处理方法有三选一:调大weight_decay、在数据增强中增加Mosaic的概率、增加训练数据量(哪怕只是复制增加样本进行简单重复,也能减缓过拟合)。
如果loss曲线下降非常缓慢甚至持平,可能是学习率设置过低,可以尝试把初始学习率调大5-10倍;也可能是模型规模太小,对复杂数据集的拟合能力不足,需要换用更大的模型版本。另外需要确认是否启用了预训练权重。YOLOv8的默认行为是在训练前下载COCO预训练权重,如果网络受限没有下载成功,模型会从零开始训练,收敛速度会明显变慢。
如果loss在某个epoch突然跳变,通常和数据增强的关闭时机有关。YOLOv8默认在最后10个epoch关闭Mosaic增强,这个时刻loss会出现一个小幅波动,属于正常现象。如果跳变幅度特别大,说明Mosaic对模型的分布影响太强,可以考虑把关闭Mosaic的时间提前到20-30个epoch。
6.3 部署推理速度优化的独门技巧
模型部署到边缘设备后,推理速度经常不够理想。除了常规的TensorRT FP16量化、RKNN INT8量化之外,还有几个容易忽略的优化点:
预处理优化,图像缩放和归一化尽量放在预处理阶段用CUDA实现,不要每次推理时在CPU上做。TensorRT的预处理网络(preprocessing plugin)可以在GPU上直接完成BGR转RGB、resize、归一化操作,减少数据搬运时间。
输出后处理优化,YOLOv8的输出解码包含置信度阈值过滤和NMS,这部分在CPU上做非常耗时。如果纯TensorRT部署,可以考虑用TensorRT自带的EfficientNMS插件,在后处理上可以省掉大量的手动解码时间。或者在C++中用多线程并行处理多路输入,充分利用CPU多核。
内存复用,在连续推理场景中避免重复申请和释放显存,统一分配到内存池里。Edge设备显存本来就小,不合理的分配策略会导致频繁的碎片和copy操作。
双路流处理,如果是多个摄像头场景,优先考虑batch推理而不是多线程推理。把两路或多路图像拼成一个batch输入到模型,GPU的利用率会显著高于单路推理。实测在RTX 5060上,batch=4的推理总耗时只比batch=1多不到30%,吞吐量提升很明显。
我自己在实际部署中感受最深的一点是:很多性能瓶颈并不在模型计算本身,而是在数据的传输和预处理上。如果把整个pipeline的数据流阶段都优化一遍,对比最初的原型代码,端到端延迟能下降50%以上,这个比例相当可观。
7. 项目总结与经验沉淀
这套交通标志检测系统做下来,我自己对YOLOv8的整个生态有了更深入的理解。模型本身只是一个引擎,真正决定项目成败的往往是数据质量、参数配置和部署方案的合理性。
几个值得记住的关键结论:
模型选型上,YOLOv8n适合边缘部署,YOLOv8s是精度与速度的平衡点,追求更高精度可以用YOLOv8m进行蒸馏后压缩。数据集上,交通标志检测一定要关注小目标问题,高分辨率输入和multi-scale训练几乎是必备手段。训练调优上,先跑通再调优,小模型验证baseline,再逐步升级模型规模和输入尺寸。部署落地时,TensorRT和RKNN是两条主流路径,量化精度损失的把控是关键。
最后再分享一点个人体会。做这种目标检测项目,最容易走的弯路就是一上来就追求最强的模型、最炫的网络结构改进,结果卡在环境配置和数据准备上出不来。我的经验是,先把一个最简单、最可靠的端到端pipeline跑通——哪怕先用YOLOv8n、用默认参数、只用少量数据跑5个epoch,只要流程完整了,后面再往里面填充优化手段就顺理成章了。每一次改动只变更一个变量,记录前后效果差异,这样调试起来最有效率。
交通标志检测技术本身已经相当成熟,但落地过程中有大量非模型类的工程问题需要解决。希望这篇文章能帮正在做类似项目的同学少踩几个坑,把精力集中在真正有价值的地方。