1. 项目缘起与整体架构思路
1.1 为什么要在边缘侧做视频分析
做过工地、厂区、园区安防项目的朋友应该都有体会:传统方案是把摄像头视频流全部拉回中心服务器,用GPU集群跑推理,再把结果推给大屏和告警系统。这套玩法在摄像头数量少的时候没问题,一旦点位上百,带宽、延迟、机房成本三座大山就压过来了。一根1080P@25fps的H.264码流大概4~6Mbps,一百路就是500Mbps左右的持续上行,这还没算解码和推理的算力开销。
边缘AI视频分析的核心思路就是把推理下沉到摄像头附近的盒子或板子上,本地解码、本地推理、本地出结果,只把结构化数据(比如“某区域某人未戴安全帽”)和少量抓拍图回传。带宽能压到原来的百分之一甚至更低,延迟从秒级降到百毫秒级,而且断网也能继续工作,这对工地这种网络环境飘忽的场景太重要了。
这个项目我选的是RK3588这颗SoC,配YOLOv8做安全帽检测。为什么是这两个组合,后面会详细拆。先给结论:RK3588自带6TOPS NPU,支持8路1080P解码,功耗控制在10W以内,单板成本可控;YOLOv8在精度和速度的平衡上比前几代更成熟,导出RKNN格式的链路也跑通了。这套组合适合中小规模的边缘部署,单板扛4~8路视频分析是比较现实的预期。
1.2 整体架构分层拆解
整个引擎我按四层来设计,从上到下依次是应用层、调度层、推理层、硬件抽象层。这样分层的好处是每一层职责清晰,换模型、换板子、换业务逻辑都不用大动干戈。
应用层负责业务规则,比如“安全帽检测结果怎么判定为告警”“告警后抓拍几张图”“推送到哪个消息队列”。这层用Python写,改起来快,业务方自己都能调。
调度层是引擎的心脏,管的是多路视频流的生命周期:哪路流该拉、哪路流该丢帧、推理任务怎么排队、NPU算力怎么分配。这层我用C++写,因为涉及大量线程和内存操作,Python的GIL会成为瓶颈。
推理层封装RKNN Runtime,负责模型加载、输入预处理、推理执行、输出后处理(NMS、坐标还原)。这层是性能关键路径,必须用C++并且尽量零拷贝。
硬件抽象层对接RK3588的MPP硬件解码器、RGA图像加速器、NPU。这层直接调Rockchip提供的底层库,把解码、缩放、格式转换的活儿交给专用硬件,CPU只做调度。
提示:分层不是目的,是为了让每一层的变更不影响其他层。我见过太多项目把业务逻辑和推理代码揉在一起,后来想换个模型或者加个告警规则,牵一发动全身。
1.3 关键选型背后的算账过程
先说为什么选RK3588而不是其他方案。当时对比了几个方向:X86+独立显卡、Jetson系列、RK3588。X86方案功耗太高,一个i5加一张入门卡轻松上60W,工地盒子要做无风扇设计基本没戏。Jetson Nano算力够但价格和供货都不太友好,Orin系列又太贵。RK3588的6TOPS INT8算力,实测跑YOLOv8n量化模型,单帧推理能压到15ms以内,这个数据后面会详细测。
再说YOLOv8。YOLOv5其实也够用,但v8的C2f结构和解耦头在同等参数量下精度更好,而且Ultralytics的导出工具链对ONNX支持很顺,转RKNN的坑少一些。安全帽检测这个任务本身不算复杂,类别就“戴帽”“未戴帽”两类(有时候加个“人”类),YOLOv8n甚至YOLOv8s就足够了,没必要上大模型。
算一笔账:单路1080P@25fps,如果每帧都推理,NPU要扛25次/秒。实测YOLOv8n在RK3588上单帧约12~18ms,理论上一秒能跑55~80帧。但实际不能这么算,因为还有解码、预处理、后处理、内存拷贝的开销。我的经验是留50%余量,单板跑4路25fps比较稳,跑6路就要抽帧到15fps左右。这个数字不是拍脑袋,后面实操部分会给实测数据。
2. 核心细节解析与实操要点
2.1 RK3588的NPU到底怎么用
RK3588的NPU是通过RKNN Toolkit2来调用的,不是直接写寄存器。流程是:PC上把ONNX模型转成RKNN格式,板子上用RKNN Runtime加载推理。这里有几个关键细节,踩过坑的都知道。
第一,量化方式的选择。RKNN支持fp16和int8量化。fp16精度高但速度慢、内存占用大;int8速度快、内存小,但精度会掉。安全帽检测这种任务,int8量化后mAP掉1~2个点通常可以接受,换来的是推理速度翻倍。我的做法是先跑fp16确认模型本身没问题,再转int8对比精度,如果掉点太多就做量化感知训练或者调整校准集。
第二,输入尺寸的确定。YOLOv8默认640x640,但RK3588的NPU对某些尺寸有对齐要求。实测640x640没问题,但如果改成非16倍数的尺寸可能会触发回退到CPU。建议保持640或416这类规整尺寸。
第三,多核NPU的利用。RK3588有3个NPU核心,RKNN Runtime支持设置核心数。单路推理时用单核延迟最低,多路并发时开多核能提升吞吐。这个要实测调,不是越多越好。
# RKNN模型加载与核心配置示例(PC端转换后的板端加载逻辑) from rknnlite.api import RKNNLite rknn = RKNNLite() ret = rknn.load_rknn('yolov8n_helmet_int8.rknn') ret = rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2) # 三核全开 # 单路低延迟场景可改为 NPU_CORE_02.2 YOLOv8模型转换的完整链路
从训练好的PyTorch模型到RKNN,中间要过ONNX这一关。链路是:.pt -> .onnx -> .rknn。每一步都有坑。
导出ONNX的时候,Ultralytics的export接口很方便,但要注意opset版本。RKNN Toolkit2对opset 12~17支持比较好,太高或太低都可能出问题。另外导出时要固定输入尺寸,动态轴RKNN支持有限。
# 导出ONNX,固定输入640x640 yolo export model=yolov8n_helmet.pt format=onnx imgsz=640 opset=12 simplify=True转RKNN的时候,配置文件里要指定量化数据集。量化数据集不是随便找几张图就行,要覆盖实际场景的光照、角度、遮挡情况。我一般从训练集里抽200~500张,再混一些现场抓拍的图。校准集质量直接决定量化后的精度,这一步偷懒后面精度崩了别怪模型。
# RKNN转换配置核心参数 rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588', quantized_dtype='asymmetric_quantized-8', optimization_level=3 )注意:mean和std要和训练时的预处理一致。YOLOv8默认是0~1归一化,所以mean=0、std=255。如果这里搞错,模型输出会完全乱掉,而且不容易发现。
2.3 视频解码与图像预处理的硬件加速
这是最容易被忽视的性能杀手。很多人模型推理优化得很好,结果解码和预处理把CPU吃满了。RK3588的MPP硬件解码器能解H.264/H.265,RGA能做缩放和格式转换,这两个必须用起来。
解码流程是:RTSP流 -> MPP解码 -> NV12帧 -> RGA缩放到640x640 -> RGB -> 送NPU。如果不用RGA,用OpenCV的resize,CPU占用会高很多。实测4路1080P解码加缩放,用RGA的话CPU占用在30%左右,纯CPU做要飙到70%以上。
RGA的使用有个细节:它支持多种格式转换,但NV12到RGB的转换要注意stride对齐。RK3588的RGA对宽度有16字节对齐要求,640是16的倍数没问题,但如果你的模型输入是其他尺寸要留意。
// RGA缩放与格式转换核心逻辑(简化示意) rga_info_t src_info, dst_info; src_info.fd = mpp_frame_fd; // MPP解码输出的DMA fd src_info.format = RK_FORMAT_YCbCr_420_SP; // NV12 dst_info.fd = rga_buffer_fd; dst_info.format = RK_FORMAT_RGB_888; imcvtcolor(&src_info, &dst_info, RK_FORMAT_YCbCr_420_SP, RK_FORMAT_RGB_888); imresize(&src_info, &dst_info, 640, 640);2.4 多路视频流的调度策略
4路视频不是简单开4个线程各跑各的,那样NPU会打架。我的做法是生产者-消费者模型:每路流一个解码线程,解码后的帧放进各自的队列;一个推理调度线程轮询队列,按优先级取帧送NPU;推理结果再分发给对应的业务处理线程。
队列长度要控制。太长会积压导致延迟增大,太短会丢帧。我一般设2~3帧的缓冲。丢帧策略也有讲究:如果队列满了,丢最旧的帧而不是最新的,保证处理的是最新画面。
抽帧策略根据业务需求定。安全帽检测不需要25fps全跑,5~10fps足够发现未戴帽行为。我一般设成每3帧取1帧,这样4路25fps实际推理负载相当于4路8fps左右,NPU压力小很多。
3. 实操过程与核心环节实现
3.1 开发环境搭建与依赖安装
板端环境是Ubuntu 20.04(Rockchip官方镜像),需要装RKNN Runtime、MPP、RGA这几个库。这些库Rockchip的SDK里都有,但版本要对齐。我踩过的坑是RKNN Runtime版本和转换工具版本不一致,导致模型加载失败。建议PC端和板端用同一版本的RKNN Toolkit2。
# 板端安装RKNN Runtime(版本要和PC端转换工具一致) sudo dpkg -i rknn-runtime_1.5.2_arm64.deb # 验证NPU驱动 cat /sys/kernel/debug/rknpu/versionMPP和RGA一般镜像里自带,如果没有要从SDK编译。编译MPP的时候注意开--enable-shared,不然链接会出问题。
3.2 模型训练与安全帽数据集准备
安全帽检测的数据集,公开的有一些,但实际项目最好自己标。现场光照、摄像头角度、安全帽颜色都影响很大。我一般标两类:helmet和head(未戴帽的人头)。有时候加person类,但person和helmet有重叠,看业务需求。
标注用LabelImg或者CVAT都行,导出YOLO格式。数据增强方面, mosaic和mixup对安全帽检测提升明显,因为现场遮挡多。但要注意mosaic可能把安全帽拼到奇怪的位置,训练后期可以关掉。
训练参数:YOLOv8n,epochs 100~150,batch 16,imgsz 640。学习率用默认的0.01配余弦退火。实测下来,5000张左右的标注数据,mAP@0.5能到0.92以上,够用了。
3.3 端到端推理管线的代码实现
核心推理函数大概长这样,我简化了错误处理和日志:
// 推理管线核心逻辑(简化) void inference_worker(int channel_id) { while (running) { cv::Mat frame = frame_queue[channel_id].pop(); // 阻塞取帧 if (frame.empty()) continue; // RGA已预处理为640x640 RGB,直接送NPU rknn_input inputs[1]; inputs[0].index = 0; inputs[0].buf = frame.data; inputs[0].size = 640 * 640 * 3; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].fmt = RKNN_TENSOR_NHWC; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, nullptr); rknn_output outputs[3]; // ... 获取输出 ... // 后处理:解码YOLOv8输出,NMS,坐标还原到原图 std::vector<Detection> dets = post_process(outputs, orig_width, orig_height); // 业务判定:有未戴帽且置信度>阈值,触发告警 for (auto& d : dets) { if (d.class_id == HEAD_CLASS && d.confidence > 0.5) { trigger_alarm(channel_id, d, frame); } } } }后处理里的坐标还原要注意:模型输入是640x640,原图是1920x1080,缩放比例不一样,x和y要分别算。letterbox的padding也要减掉。这个算错的话框会偏。
3.4 性能实测数据与调优记录
实测环境:RK3588开发板,4路1080P RTSP流,YOLOv8n int8量化,640x640输入。
| 配置项 | 单路延迟 | 4路CPU占用 | 4路NPU占用 | 备注 |
|---|---|---|---|---|
| 全帧推理25fps | 45ms | 85% | 95% | 延迟高,有积压 |
| 抽帧8fps | 38ms | 45% | 55% | 推荐配置 |
| 抽帧5fps | 35ms | 32% | 38% | 低功耗场景 |
| 单核NPU 8fps | 52ms | 44% | 60% | 延迟略高 |
| 三核NPU 8fps | 38ms | 45% | 55% | 推荐 |
调优过程中发现几个点:一是RGA缩放比OpenCV快3倍以上;二是NPU三核全开在4路并发时吞吐最好,单路时反而单核延迟低;三是内存拷贝是隐形杀手,尽量用DMA fd零拷贝。
提示:实测数据会因板子散热、内存频率、流分辨率不同有波动。建议在自己的硬件上复测,不要直接抄数字。
4. 常见问题与排查技巧实录
4.1 模型转换与加载类问题
问题一:ONNX转RKNN报不支持的算子。这个最常见。YOLOv8的一些算子RKNN Toolkit2可能不支持,比如某些版本的SiLU激活。解决办法是导出ONNX时用simplify,或者换opset版本。如果还不行,就要改模型结构,把不支持的算子替换掉。
问题二:模型加载成功但推理结果全乱。九成是预处理参数不对。检查mean/std、输入格式(NHWC还是NCHW)、量化方式是否匹配。我遇到过一次是量化时用了RGB校准集,推理时喂了BGR,结果全错。
问题三:RKNN Runtime版本不匹配。报错通常是“invalid model”或“version mismatch”。PC端转换工具和板端Runtime版本必须一致,这个没有捷径,对齐版本。
4.2 视频解码与流媒体类问题
问题一:RTSP流频繁断连。工地网络不稳定是常态。我的做法是加断线重连机制,检测到连续N帧解码失败就重建连接。另外RTSP的TCP/UDP模式要选对,UDP在丢包网络下花屏严重,建议用TCP。
问题二:解码延迟越来越大。通常是队列积压。检查推理速度是否跟不上解码速度,如果是就抽帧或者降分辨率。另外MPP解码器的缓冲区要设合理,太小会丢帧,太大会增加延迟。
问题三:多路流时某路卡死。可能是那路流的编码格式特殊,或者分辨率不标准。加日志看解码器返回的错误码,必要时对该路流做转码预处理。
4.3 性能与稳定性类问题
问题一:跑一段时间后NPU占用率下降。检查是否内存泄漏。RKNN的输入输出buffer要记得release,不然会累积。用valgrind或者板端的内存监控工具查。
问题二:夏天高温降频。RK3588满载温度能到70度以上,没有散热措施会降频。工地盒子要做散热设计,至少加个铝壳和导热垫。实测加散热片后连续跑24小时不降频。
问题三:告警误报多。安全帽检测的误报主要来自反光、遮挡、相似颜色。解决办法:提高置信度阈值、加跟踪算法(连续N帧都检测到才告警)、加区域过滤(只检测作业区域)。我一般用ByteTrack做简单跟踪,误报能降一半以上。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 模型加载失败 | 版本不匹配 | 查Runtime和Toolkit版本 | 对齐版本 |
| 推理结果乱 | 预处理参数错 | 查mean/std/格式 | 对齐训练预处理 |
| 解码花屏 | 网络丢包 | 查RTSP模式 | 改TCP |
| 延迟增大 | 队列积压 | 查推理耗时 | 抽帧/降分辨率 |
| NPU占用下降 | 内存泄漏 | 查buffer释放 | 加release |
| 高温降频 | 散热不足 | 查温度 | 加散热 |
| 误报多 | 阈值低/无跟踪 | 查告警日志 | 提阈值/加跟踪 |
5. 工程化落地的一些经验
5.1 部署与运维的实际考量
边缘盒子部署到现场,最头疼的不是技术,是运维。设备分散、网络差、没人会调试。我的做法是:第一,做远程配置和升级能力,能远程改阈值、换模型、重启服务;第二,做本地日志轮转,别把存储写满;第三,做看门狗,进程挂了自动拉起。
模型更新是个麻烦事。我的方案是模型文件放独立分区,升级时只替换模型文件,不动系统。升级包做校验,防止传输损坏。升级失败要能回滚,不然现场设备变砖就麻烦了。
5.2 成本与功耗的平衡
单板4路方案,RK3588核心板加底板、电源、外壳,物料成本能控制在合理范围。功耗实测满载12W左右,加散热和电源损耗,整机15W以内。这个数字对太阳能供电或者PoE供电的场景很关键。
如果要跑更多路,有两个方向:一是换更高算力的芯片,二是多板级联。我的经验是单板超过8路后,解码和内存带宽会成为瓶颈,不如多板分摊。多板的话要做统一管理,每块板子跑自己的分析,结果汇总到中心。
5.3 后续可扩展的方向
这套引擎的架构是通用的,换模型就能做别的检测:反光衣、安全带、烟火、区域入侵。推理层封装好了,换RKNN模型就行。调度层支持动态加流,业务层用插件方式加载规则。
还可以加视频结构化,把检测到的人、车、物做特征提取和检索。RK3588的NPU也能跑轻量ReID模型,不过算力要重新分配。这个方向适合有检索需求的场景,比如找人、找车。
最后分享一个小技巧:调试阶段一定要把中间结果可视化出来,解码帧、预处理后的图、模型输出框,都存下来看。很多问题看日志看不出来,看图一眼就明白了。我习惯在代码里加个debug开关,打开就存图,关掉就不存,不影响性能。