ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RK3588+YOLOv8边缘AI视频分析引擎:安全帽检测落地优化

RK3588+YOLOv8边缘AI视频分析引擎:安全帽检测落地优化

做安全帽检测这类项目,大家最先想到的往往是“拿 YOLOv8 训练完就万事大吉”,但放到 RK3588 这类边缘设备上,模型训练只是很小的一段。真正决定项目能不能落地的,是视频流、硬件解码、NPU 推理、告警上报这一整套链路怎么拆、怎么串、怎么优化。我这篇文章就把 RK3588 + YOLOv8 做边缘 AI 视频分析引擎的核心架构拆开讲一遍,重点落在安全帽检测这个场景上,也涵盖多数同类视觉项目的通用设计思路。

我接手过不止一个类似的工厂/工地巡检项目,最大感受是:在一个边缘盒子里塞模型很简单,塞一整套“视频分析服务”才是难点。摄像头角度一变、光线一变、帧率略有波动,整个系统就可能从“能跑”变成“会崩”。所以这篇文章适合正在做边缘检测方案选型、准备把模型部署到 RKNN 平台、或者已经跑通 Demo 但被实际工程问题卡住的朋友参考。

1. 边缘 AI 视频分析引擎先拆成四层再谈其他

先说一个常见的误区:很多人的 Demo 是“Python 读一帧图片 → 推理 → 画框”,然后把这套逻辑直接套到视频流上。这在离线测试时没有任何问题,一旦面对多路 RTSP 流,CPU 占用、内存释放、线程阻塞、丢帧卡顿全来了。原因不是模型不行,而是整个系统没有分层。

我习惯把边缘 AI 视频引擎拆成四层:

  • 接入层:负责连接摄像头、拉取 RTSP/ONVIF 流、断线重连、流媒体协议处理;
  • 解码与图像处理层:做硬解、缩放、格式转换,产出 NPU 能吃的输入张量;
  • 推理层:加载 RKNN 模型、执行推理、把输出解析成目标框;
  • 业务层:安全帽状态判定、告警截图、事件上报、规则过滤。

接入层和业务层最容易被人省略,但恰恰是工程上最费时间的部分。比如摄像头支持 H.265/HEVC 还是老旧的 MJPEG,直接影响解码方案选型;再比如安全帽告警“连续 10 秒未戴帽”这种业务规则,如果不放到业务层做状态缓存,而是由模型每帧生硬输出,误报率会高到让现场负责人直接关掉系统。

分层还有一个实际好处:模型更新时只替换推理层,不需要动接入和解码;摄像头品牌更换时只动接入层,模型重新训练后也不需要改解码。四层之间用队列或者回调解耦,调试某一层时,其他层可以用 mock 数据顶上。我搭建的架构大概是这样的:

RTSP 拉流 → MPP 硬解 → Resize/色彩空间转换 → RKNN 推理 → 后处理/NMS → 业务规则过滤 → 告警/截图/上报

这个单向管线如果是纯顺序执行,必然出现“解码比推理快”或者“推理比解码快”导致的资源浪费。成熟的做法是解码线程和推理线程分开,用有界缓冲区衔接,既保证摄像头侧不会因为处理不及时而崩溃,也让 NPU 尽量满负荷运转。

2. 选型 RK3588 而不选 NVIDIA Jetson 的真实理由

很多团队一提到边缘 AI 就会想 Jetson,但实际做安全帽检测这类中等复杂度模型时,RK3588 是性价比很稳的选择。不是说 Jetson 不好,而是要从成本、功耗、量产供货、算法部署难度几个维度综合看,RK3588 在国内供应链里的可操作性更强,开发资料也更贴近工业级应用。

2.1 RK3588 的异构算力构成和硬解模块

RK3588 是一颗 8 核 ARM SoC,构成为 4 个 Cortex-A76 大核加 4 个 Cortex-A55 小核,另外带 Mali-G610 GPU 和 3 个 NPU 核心,NPU 总算力在 INT8 精度下单看标称能做到 6 TOPS 左右。对于 YOLOv8n 这类轻量模型,把分辨率控制在 640x640 时,单路推理耗时可压到 20~40ms 区间,性能足够覆盖普通监控场景。

比算力更关键的是它的视频编解码能力。RK3588 支持 8K 视频硬解,1080p 这类常规监控分辨率更是不在话下。硬件解码会把 CPU 从 FFmpeg 软解的负担里解放出来,而这个能力在边缘视频引擎里比 GPU 浮点算力还重要。很多摄像头输出的 1080p 30 帧码流,如果全部用 CPU 软解,A76 大核会被吃掉一两个,NPU 就算有资源也被解码拖累。

2.2 与 Jetson、树莓派的对比差异

平台优势明显短板适合场景
RK3588供货稳定、性价比高、硬解强、RKNN 工具链成熟模型生态不如 CUDA 丰富,算子支持有限多路视频流、中等模型、工业级量产盒子
Jetson 系列CUDA/TensorRT 生态成熟,模型兼容性极好成本高、部分型号功耗和散热压力大复杂模型、高精度要求、研发验证
树莓派 5上手简单、资料多NPU 需要外挂,整体推理吞吐低原型验证、轻量演示

实际选择时还要看目标环境。工地或工厂的配电条件通常不理想,设备经常要在 12V 或者 24V 供电环境下工作,RK3588 的功耗控制在整板 10W 左右就能跑起来;Jetson Orin 系列的功耗墙更高,散热结构设计不好容易降频。我也见过团队为了追求性能强行上 Jetson,结果现场没有合适电源,被迫加装工业电源模块,成本和故障点都上去了。这类“外围成本”在做选型时一定要算进去,不能只盯算力表。

3. YOLOv8 在 Rockchip 的部署链路:从 PyTorch 到 RKNN

YOLOv8 训练通常是在 PyTorch 环境完成的,而 RK3588 的 NPU 只能用 RKNN 格式的模型。这中间要经过“导出 ONNX → 量化校准 → 转成 RKNN”三步。很多新人在第一步就卡住,搞了几天还在报算子不支持的错,所以我把关键步骤和容易踩的细节展开讲。

3.1 转换前先管好 ONNX 导出

先说 YOLOv8 的导出。官方仓库提供了export.py或者 Python API,但我建议导出 ONNX 时不要贪多,把 NMS 剥离出去,只导出模型的主干,也就是检测头的原始输出。原因有两点:一是 RKNN 工具链对自定义 NMS 算子支持不稳定,强行转换成 RKNN 时经常会遇到不支持节点;二是 NMS 这类后处理放到 CPU 上做,灵活度更高,后续改置信度阈值、IoU 阈值都不用重新转换模型。

导出 ONNX 时还需要固定输入尺寸。如果你的目标场景是 16:9 的监控画面,千万不要无脑保持 640x640 正方形输入,可以按实际画面做预处理后 padding 到 640x640,或者直接把输入尺寸改成 640x416、640x384 这类接近真实画幅的尺寸,减少无效填充区域。固定尺寸后,后续 RKNN 端也更容易做量化校准。

3.2 rknn-toolkit2 转换与 INT8 量化

RK3588 对应的转换工具是 rknn-toolkit2,一般跑在 x86 PC 上。转换时指定 target_platform 为 rk3588,加载 ONNX 后设置量化配置。核心代码并不复杂:

from rknn.api import RKNN rknn = RKNN() ret = rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588' ) ret = rknn.load_onnx(model='yolov8n.onnx', output_size=([1, 84, 8400])) ret = rknn.build(do_quantization=True, dataset='dataset.txt') rknn.export_rknn('helmet.rknn')

注意mean_values和std_values必须与训练时预处理对齐,YOLOv8 常规做法是图像除以 255 后归一化到 0~1,对应 mean 为 0、std 为 255。这里如果写错,跑出来的检测框位置会偏移,置信度也会整体失真。

dataset.txt里放的是校准图片路径,每行一张,工具会用这些图片统计各层的激活值范围,从而做 INT8 量化。安全帽检测的量化数据集最好从真实监控画面里抽帧,尽量覆盖不同角度、光照和远近尺度。我见过有人用纯训练集图片做量化,结果模型在训练集上很准,一到现场就形状怪异、目标漏检,原因就是校准图片分布和部署场景偏差太大。

3.3 RKNN 输出的后处理

YOLOv8 导出的原始输出是三维张量,一般形状为[1, classes+4, 8400]或者类似布局。8400 代表在三个尺度特征图上的候选框数量,通常由 80x80、40x40、20x20 相加得到。RKNN 不会帮你把候选框解码成最终的 x1,y1,x2,y2,也不会替你做 NMS,这些都要在 CPU 侧自己补上。

网上很多代码是从 Ultralytics 仓库直接抄来的后处理逻辑,但在 RKNN 上不能盲用。因为 RKNN 输出的布局、通道顺序可能和 PyTorch 的原始输出不完全一致,需要先打印输出 shape 做人工对齐。我的建议是:

  • 解析流程分为步:取每个候选框的 x、y、w、h,通过置信度过滤,再做类别维度 argmax 得到类别和置信度;
  • 用共享 IoU 的方式合并重叠框,类别相同的框才参与 NMS;
  • 最后把坐标按输入缩放比映射回原视频帧坐标。

置信度阈值不要太低。监控场景画面变化小、重复对象多,0.5 以下是各类误报的温床。我通常设到 0.55 或 0.6,宁可偶尔漏检,也不要让误报淹没告警通道。

4. 实现实时视频流识别:拉流、硬解、多路并发

模型转换好之后,真正的工程量在怎么把“单帧推理”变成“每秒 20 帧以上的连续分析”。

4.1 拉流和解码线程的设计

RK3588 上最简单的取流方式是用 FFmpeg 命令拉 RTSP,但生产环境我不会直接拿命令拼接。因为命令方式很难处理流中断、自动重连和多路并发。更可靠的方案是用 FFmpeg 的 C API 或基于 GStreamer 的复用框架,在程序内部管理拉流线程。

每一个摄像头对应一条独立的拉流线程,线程里做三件事:连接流、读取编码帧、把帧交给硬件解码器。解码后的帧不能直接给 NPU,先要做尺寸缩放和色彩空间转换。这时候要用到 Rockchip 的 MPP(Media Process Platform)库,它提供硬件加速的缩放和转码能力。RK3588 的硬解模块效率很高,一路 1080p 解码加缩放对 CPU 的占用率可以压得很低。

线程之间用队列传递对象,注意这里是“对象”而不是“整帧数据拷贝”。我在早期版本中直接把 numpy 数组塞进队列,结果 4 路视频就把内存吃爆了。后来改成传递帧对象的元数据或引用,并在消费完成后手动释放,内存占用才稳定下来。

4.2 多路并发与帧丢弃策略

多路并发时,最怕的是某一路解码速度慢,导致其他路排队。我的处理方式是在拉流线程和解码线程之间放一个环形缓冲,缓冲满时直接丢弃新帧,而不是阻塞拉流。视频分析任务允许丢帧,不允许积压,因为积压只会让延迟越来越大,最终画面和控制台显示脱节。

另外,同一个 NPU 模型实例不要被多个线程并发调用。RKNN 的 Python API 对并发支持不如 C API,我实测过来,多线程同时调rknn.inference偶尔会导致进程崩溃或输出错乱。稳妥的做法是:每路视频对应一个独立推理线程,或者用线程池串行化推理,再或者创建多个 RKNN 实例分担压力。至于帧丢弃,我的经验是优先丢弃帧间隔太密的帧,时间上均匀地取样。这样既保证了实时性,也能维持相对平滑的检测频率。

关于输入尺寸再补充一点。1080p 视频直接缩放成 640x640,视角中等距离下的安全帽大约只占十几个像素,小目标检测压力会很大。一种做法是把 ROI 区域单独抠出来放大再推理;另一种是输入分辨率保持 1280,但要付出更大的 NPU 耗时。实际项目里我一般保留一个可配置项:白天用 960,夜间用 640,兼顾距离和能耗。

5. 精度、帧率和稳定性怎么平衡

安全帽检测涉及的误报问题,很多时候不是模型本身问题,而是目标太小、对比度低、画面模糊,或者训练集和现场环境差别太大。到了 RK3588 上,还要考虑 INT8 量化带来的精度损失,这一节说说我的平衡思路。

5.1 校准集和混合量化

先说量化。YOLOv8n 从 FP16 降到 INT8,安全帽这种小目标的边界框定位精度通常会轻微下降。如果下降不严重,完全可以用 INT8 换来近一倍的推理速度。但如果你发现自己的安全帽检测在 15~30 米距离外明显漏检,那就要考虑混合量化方案:让检测头甚至一部分关键层保持 FP16,其余层用 INT8。

rknn-toolkit2 提供了按层指定量化开关的方式,可以手工指定某些层不量化。实际操作时,我会先用默认 INT8 跑一遍,统计一下模型输出置信度分布,找出最容易漏检的目标尺度,再看它主要在哪个特征层输出,把对应层的量化关掉。这个过程的调参周期会比较长,但性能收益非常直接。

5.2 模型选型和输入尺寸的降级路线

如果现场多路并发,NPU 算力吃紧,我不建议立刻换更大的模型,反而会先看输入分辨率能不能降。把 640 降到 416,如果不考虑 padding,理论上推理耗时能降到原来的 40% 左右。对近距离安全帽识别场景,416 大小往往足够用。

输入分辨率降到极限后还不够,再考虑换模型。YOLOv8n 是轻量级里性价比最高的一个,如果连它都跑不动,可以尝试更大步长剪枝的定制版本,或者直接用 YOLOv8 官方仓库里的小输入配置重新训练。反过来,如果精度不够,不要一上来就换 YOLOv8s,先检查数据集有没有覆盖现场角度和光线,多数精度问题出在数据而非模型容量。

我和团队做过一组对比:同一个安全帽检测项目,用 YOLOv8n + INT8 在 RK3588 上跑,单路推理约 25ms,4 路视频同时分析时整体帧率能稳定在 15 FPS 以上;换成 YOLOv8s,精度确实提升了几个点,但 4 路并发就降到 8 FPS,现场画面明显不跟手。最终方案还是用回轻量模型,靠优化数据来补精度。

6. 上线之后容易被低估的坑:色彩空间、线程、误报与稳定性

这部分全是我自己踩过的坑,每一个在文档里都不起眼,到了现场都足够让人折腾半天。

6.1 NV12 转 RGB 引发的“变色”问题

IPC 摄像头输出到 RK3588 硬解后,常见的像素格式是 NV12。NV12 是 YUV 420 半平面格式,Y 分量一块,UV 分量交错一块。如果直接把 NV12 数据交给 RKNN 推理,或者转换顺序写错,画面上的人会泛紫、泛绿,检测框倒还在,但模型置信度会比正常画面低很多。

这个坑最折磨人的是“看起来像颜色偏了一点,但转出来的框还在动”。我记得有次排查了半天,最后才发现是转换时把 NV12 的 UV 顺序当成 VU 处理了。解决方式是统一用 MPP 提供的转换接口,或者用一套经过验证的 NV12 → RGB 转换函数,绝不要在每个线程里各写一套。色彩空间转换要放在解码完成后的固定环节统一做,并且转换结果直接用 float 张量送入 RKNN,中间不要再经过 JPEG 编码保存。

6.2 线程数量:不是越多越好

接入层、解码层、推理层如果每个模块都开线程,一多就会出现“线程多、CPU 排队、延迟反而更高”的现象。RK3588 的大核只有 4 个,拉流解码和业务上报会抢占 CPU,如果每路视频各开 4 个线程,6 路就是 24 个线程,线程切换开销会吃掉不少性能。

我推荐的控制节奏是:拉流线程数和视频路数一致,解码线程可以共享一个固定线程池,推理线程按 NPU 实例数设置,业务上报单独一个队列和消费者线程。不要因为觉得线程池名字很酷就无脑加大最大线程数,性能瓶颈不是线程不够,而是队列、锁和内存带宽。

6.3 安全帽判定误报:借助业务规则过滤

模型最终输出的只有“人头”、“帽子”这类检测框。判定工人是否戴安全帽,通常做法是用头部框和帽子框计算交并比 IoU。帽子框与头部框重叠度大于某个阈值,判定为“已佩戴”,否则是“未佩戴”。这个阈值我在项目里取 0.3 左右,太严容易把戴歪的帽子判成未戴,太松又会漏掉明显未戴的情况。

但真正现场能不能用,关键还在业务层过滤。单帧误判不能直接触发告警,我会做 3~5 帧连续判定,比如连续 3 帧未戴帽才产生告警;同时对同一工人做去重,防止 6 路视频交叉覆盖时反复告警。还要设置时间段规则,比如夜间无人施工时不发告警。这些规则完全是工程问题,却往往是客户验收时最看中的部分。

6.4 散热与长时间稳定性

RK3588 在持续高负载推理时,温度控制不好会降频。有的盒子塞在户外弱电箱里,夏天箱内温度轻松超过 40 度,设备跑半小时就开始降频,推理时间从 25ms 变成 60ms,体验断崖式下滑。

我的建议是上线前做 24 小时压力测试,观察 NPU 温度和推理延时曲线。如果温度持续超过 75 度,就需要加散热片或风扇,或者把设备的推理频率限制在 2 核 NPU,牺牲一点算力换取稳定。很多时候“性能不够”其实是散热不够,而不是芯片能力不够。

再提一个容易忽略的稳定性问题:断网续传。工地现场网络不一定稳定,边缘盒子要能先把告警事件保存在本地,恢复网络后再补报。这个功能看起来不显眼,但很多项目验收时网络环境很差,没有断网续传机制几乎没法通过连续测试。

前面这些点做完,整个安全帽检测方案才算真正完成了从“模型实验”到“边缘产品”的转变。如果要我总结一条经验,那就是在 RK3588 这类平台上做项目,模型精度是入场券,视频解码、线程模型、业务过滤和稳定性设计才是真正拉开差距的地方。每个环节都有大量经验性细节,没有捷径,只能一项项打磨。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进