ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RK3588实战:用RGA硬件加速图像预处理,告别CPU瓶颈

RK3588实战:用RGA硬件加速图像预处理,告别CPU瓶颈 先聊个现象。最近在RK3588上跑视觉项目最直观的感受是这块芯片的NPU算力确实猛6 TOPS摆在那跑YOLOv8的s版本可以很轻松但真到了做产品落地很多人会发现瓶颈根本不在NPU而在图像预处理那一大段。摄像头出来的数据是NV12、BGR、MJPEG各种格式进推理框架之前要先缩放、再转RGB、有时候还要旋转裁剪。这一套组合拳如果全扔给CPU硬扛1080P的一帧处理下来十几毫秒就没了哪怕NPU推理只要5毫秒整个流水线还是被预处理卡死。后来我把预处理切到RGA硬件加速库上同样是1080P转640x640处理时间直接压到1毫秒上下CPU占用率从百分之六七十掉到个位数。这篇就是把我在RK3588上折腾RGA的完整记录整理出来接口怎么调、内存怎么对齐、哪些格式组合会踩坑都一并说清楚给同样在搞RK3588视觉方案的朋友做个参考。1. RGA到底是什么为什么视觉项目离不开它1.1 RK3588的硬件加速矩阵里RGA扮演什么角色RK3588是一颗典型的全家桶SoC4颗A76大核加4颗A55小核NPU算力6 TOPSGPU是Mali-G610VPU支持8K编解码。很多人只盯着NPU看却忽略了芯片上还有一票专用硬件单元其中RGA就是一个存在感不高但极其重要的角色。RGA全称Raster Graphic Acceleration直译是光栅图形加速本质是一个2D图像硬件加速引擎。它不负责渲染3D场景也不负责神经网络推理它干的事情非常专一图像缩放、格式转换、旋转、镜像、裁剪、颜色空间转换、alpha混合。这些操作在视觉流水线里出现的频率极高而且计算模式高度固定非常适合用专用硬件流水线去跑。打个比方CPU就像一个多才多艺的师傅什么活都能揽但每单活都要重新沟通、重新准备工具RGA则是一条专线传送带只处理图像搬运和变形这几种固定操作你把图放到入口参数设好它轰轰隆隆几毫秒就把活干完了。在RK3588的架构里NPU负责推理算力GPU负责复杂渲染VPU负责编解码RGA负责的是图像数据在各个环节之间的精加工和转车。视觉流水线里的预处理环节刚好就落在RGA头上。我在实际项目里体会到RK3588这颗芯片的每一块硬件单元都对应一个典型的业务场景如果你把图像预处理这种重复性极高的操作压在CPU上A76大核再多也不够用因为一帧图像转格式涉及几百万次像素访问纯软件循环的效率远不如专用硬件。而RGA这种固定功能硬件功耗低、时延小、不占CPU对于高分辨率视频流的实时处理是刚需。1.2 RGA能干的活比你想的多RGA处理的基本元素是图像buffer所有操作都可以归类为从源buffer读数据经过变换写入目标buffer。单次RGA操作可以同时组合缩放、格式转换、旋转、裁剪、镜像等多个属性不需要拆成多次调用。这一点非常关键意味着你可以把三步操作压成一次硬件调用。具体能力可以列一下图像缩放支持任意比例的放大缩小输出宽高由目标矩形决定比如4K缩到640x640也可以只缩到640x480这种非等比尺寸。格式转换支持RGB/RGBA/BGR各通道顺序、YUV的各种采样格式NV12、NV21、YUV420P、YUV422等之间的互相转换这是摄像头数据接入视觉模型最常用的功能。旋转与镜像支持90度、180度、270度旋转以及水平、垂直镜像。裁剪与ROI可以从大图中抠出一块区域做后续处理配合缩放可以实现缩放裁剪的效果。Alpha混合支持两路图像叠加混合常用于UI叠加和视频合成。在我做USB摄像头转RTSP推流的项目里RGA的作用非常典型。UVC摄像头输出的通常是YUYV或者MJPEG格式要推RTSP流一种做法是软解码成YUV再喂给编码器另一种做法是先用RGA把YUV转成编码器最擅长的NV12格式顺便把分辨率统一成720P或1080P然后在编码器侧做硬编。这样整条链路都是硬件在跑CPU占用几乎可以忽略。1.3 RGA和CPU处理的实际差距说个实测数据。在RK3588上用CPU纯C代码把一张1920x1080的NV12图像转成RGB888再缩放到640x640普通优化大概需要15到20毫秒哪怕上NEON指令集优化也得8到10毫秒。同样的操作交给RGA一次imresize调用同步完成实测在1毫秒左右。这个差距不是简单的快了一点点而是量级的差别。假设你跑YOLOv8sNPU推理一帧大约10毫秒出头如果用CPU做预处理整帧延迟直接变成30毫秒帧率只能做到30FPS多一点换RGA预处理整帧延迟控制在15毫秒以内跑到60FPS都没有压力。在视频实时分析这种场景里这十几毫秒就是能不能用的分水岭。2. 环境准备把librga跑起来的前置工作2.1 硬件运行环境怎么选RGA是RK3588芯片内置的硬件模块只要芯片是RK3588不管跑什么系统RGA都真实存在。但能不能用、好不好用取决于软件包里有没有带RGA的用户态驱动库。我试过三种环境官方SDK编译的Ubuntu系统最省心SDK已经把librga相关的头文件和动态库打进系统了直接开发就行。Armbian系统大部分基于RK3588的Armbian镜像也带了RGA驱动但用户态librga不一定默认安装需要自己装或者从源码编译。Android系统Android上也有RGA的HAL封装但应用层跨调用比较麻烦通常是在C层通过hardware module访问做系统级优化时才会碰。对于做视觉算法的朋友首选是Ubuntu Server或带桌面的Ubuntu原因很简单生态成熟、gcc/cmake齐全、调试方便。RK3588的Ubuntu系统默认就映射了/dev/rga这个设备节点这是RGA驱动的用户态入口。先检查一下设备节点ls -l /dev/rga如果能看到类似crw-rw---- 1 root video 10, 110的输出说明RGA驱动已经就位。权限不够的话把当前用户加到video组sudo usermod -aG video $USER重新登录后就能直接访问了。2.2 librga的获取与版本选择用户态操作RGA的库叫librga在RK3588上配套的是较新的版本提供了一套IM2D2D Image Manipulation接口。这里有一个特别重要的点librga的老接口和新接口差异非常大。老版本使用rga_info_t结构体配合rk_rga_blit函数暴露的细节多门槛高而且很多博客教程写的是这种老接口直接抄过来在新SDK上很可能编译不过。新版本推荐IM2D接口用rga_buffer_t描述buffer用imresize、imconvert、imrotate这类语义化函数代码简洁不少对新手友好得多。怎么获取librga两种途径SDK自带在瑞芯微SDK的external/目录下一般有librga源码编译整个SDK时会自动装到系统里。从源码独立编译可以从开源仓库拉取librga代码编译安装到目标板。编译librga的过程不复杂建议在开发板上直接编省得交叉编译的麻烦mkdir -p build cd build cmake .. make -j4 sudo make install装完之后系统里会有/usr/include/rga目录头文件包括rga.h、im2d.h等动态库是librga.so。确认一下ls /usr/include/rga ls /usr/lib/librga.so*如果你的系统版本比较老可能会遇到找不到im2d.h的情况那说明librga版本太旧建议从源码升级新版。2.3 写第一个程序时的编译配置在CMakeLists.txt里关键是找到头文件和库。find_path(RGA_INCLUDE_DIR NAMES rga/im2d.h) find_library(RGA_LIBRARY NAMES rga) include_directories(${RGA_INCLUDE_DIR}) target_link_libraries(your_target ${RGA_LIBRARY})RGA库本身依赖libdrm链接的时候一般不需要手动加因为librga会传递依赖但如果你用到了drm相关的buffer管理函数也需要单独找libdrm。到这里环境就绪了下面开始写代码。3. 从调用接口开始RGA编程模型3.1 四个核心概念先理清楚RGA编程模型不复杂但要先理解四个概念。第一个是源buffer和目标buffer。RGA操作基本都有输入和输出输入描述的是原图像长什么样输出描述的是目标图像要长成什么样它们格式可以不同尺寸可以不同。第二个是关键属性width、height和stride。width/height好理解stride是很多新手栽跟头的地方。stride指的是内存中一行像素实际占用的字节数或像素数它不一定等于width。摄像头、GPU、显示控制器分配内存时通常会对行做对齐比如一行1280像素的NV12实际stride可能是1280、1284或1288取决于对齐方式。如果直接按width处理画面就会出现斜条纹或者颜色偏移。第三个是format。RGA使用RK_FORMAT_XXX系列宏来表示格式比如RK_FORMAT_RGB_888、RK_FORMAT_RGBA_8888、RK_FORMAT_YCrCb_420_SP对应NV12。格式定义在rga.h里使用前务必搞清楚摄像头输出的到底是什么格式搞错了RGA直接报错或者出花屏。第四个是buffer的来源。RGA支持三种来源虚拟地址、物理地址、dma_buf文件描述符。虚拟地址最简单直接传指针物理地址需要驱动层才能拿dma_buf fd最安全也是我在项目中推荐的方式因为它能正确处理跨硬件访问时的cache同步问题。3.2 用IM2D接口完成第一次缩放用IM2D接口写一个缩放程序代码核心只有几步。#include stdio.h #include stdlib.h #include string.h #include im2d.h #include rga.h int main() { int src_w 1920, src_h 1080; int dst_w 640, dst_h 640; void *src_ptr NULL; void *dst_ptr NULL; src_ptr malloc(src_w * src_h * 3 / 2); dst_ptr malloc(dst_w * dst_h * 3); if (!src_ptr || !dst_ptr) { printf(malloc failed\n); return -1; } memset(src_ptr, 0, src_w * src_h * 3 / 2); memset(dst_ptr, 0, dst_w * dst_h * 3); rga_buffer_t src wrapbuffer_virtualaddr(src_ptr, src_w, src_h, RK_FORMAT_YCrCb_420_SP); rga_buffer_t dst wrapbuffer_virtualaddr(dst_ptr, dst_w, dst_h, RK_FORMAT_RGB_888); IM_STATUS ret imresize(src, dst); if (ret ! IM_STATUS_SUCCESS) { printf(imresize failed, status%d\n, ret); return -1; } printf(RGA resize ok\n); free(src_ptr); free(dst_ptr); return 0; }这段代码做的事情是读入一张1920x1080的NV12数据在RGA硬件内部完成从NV12到RGB888的格式转换同时把尺寸缩放到640x640输出到dst_ptr指向的内存里。缩放和格式转换这两件事RGA一次调用就完成了。很多人会疑惑直接用虚拟地址就行吗对于带MMU的RGA版本来说是可以的RK3588上的RGA通过内部MMU访问虚拟地址映射和cache同步由驱动处理。不过对于性能要求比较高的场景我更推荐使用dma_buf fd的方式这个后面说。3.3 输出路径上有哪些坑wrapbuffer_virtualaddr这个系列函数还有一个带stride参数的版本非常重要rga_buffer_t src wrapbuffer_virtualaddr_t(src_ptr, src_w, src_h, src_wstride, src_hstride, RK_FORMAT_YCrCb_420_SP);如果你的图像内存不是紧密排列的比如从DMA buffer里映射来的帧每行有额外的padding就必须把wstride传进去。否则RGA按紧密排列去读取出来的图像就是歪的。还有一个坑RGA的stride对齐要求在不同系统版本上不一样。有些版本要求16像素对齐有些要求64字节对齐。最稳妥的做法是不管内存怎么来的都把你的wstride显式设置为对齐后的值。比如你实际width是1280但镜头分配的内存stride是1280就直接用1280如果你的buffer每行实际占1296字节就老老实实传1296。format的坑也很常见。RK_FORMAT_YCrCb_420_SP对应的是NV12RK_FORMAT_YCbCr_420_SP对应NV21这俩容易搞混。NV12是YUV420半平面格式UV分量排列是U在前V在后NV21相反。摄像头驱动里最常输出NV12或者NV21YOLOv8预处理阶段一般要转成RGB我用NV12比较多代码里也要保持一致。4. 实战YOLOv8部署里的预处理加速4.1 一条完整的推理链路应该怎么设计RK3588上部署YOLOv8典型链路是这样的摄像头采集视频帧经过RGA做缩放、格式转换、必要的裁剪得到模型输入尺寸的RGB数据然后通过rknn_inputs_set喂给NPUNPU推理完输出结果最后后处理画出检测框。这一步里预处理放在哪里执行直接决定了整条流水线的性能。我最早偷懒用OpenCV的cvtColor和resize一条1080P NV12转640x640 RGB的流程在RK3588上CPU占用率飙到70%以上帧率还不稳定。后来把预处理全部切到RGA情况完全改观。4.2 用RGA接管模型的输入预处理假设摄像头输出NV121920x1080模型输入是640x640 RGB参考逻辑如下。// nv12_ptr来自摄像头驱动/解码器 // model_input_ptr指向rknn_input的buffer尺寸是640*640*3 rga_buffer_t src wrapbuffer_virtualaddr(nv12_ptr, cam_w, cam_h, RK_FORMAT_YCrCb_420_SP); rga_buffer_t dst wrapbuffer_virtualaddr(model_input_ptr, 640, 640, RK_FORMAT_RGB_888); if (imresize(src, dst) ! IM_STATUS_SUCCESS) { LOGE(rga preprocess failed); }然后模型推理rknn_input input; memset(input, 0, sizeof(rknn_input)); input.index 0; input.type RKNN_TENSOR_UINT8; input.size 640 * 640 * 3; input.fmt RKNN_TENSOR_NHWC; input.buf model_input_ptr; input.pass_through 0; rknn_inputs_set(ctx, 1, input); rknn_run(ctx, nullptr);注意rknn_input.buf直接指向RGA的输出地址中间没有用memcpy再拷一道减少了内存拷贝的开销。如果你担心RGA写入和NPU读入之间会有cache一致性问题在实际测试中RGA和RKNN在RK3588的同一个统一内存架构里配合得不错只要保证buf指针有效且等待RGA操作完成即可。IM2D接口的imresize默认是同步等待完成的也就是说调用返回时目标buffer里的数据已经可用了不需要额外做同步。这在大对数应用里很省心。4.3 多路摄像头场景下的调度如果项目是4路甚至8路摄像头同时做检测每路的预处理都调RGA会怎么样RGA硬件只有一个处理单元多个线程同时调用时驱动会在内核层排队。实测下来RGA排队处理4路1920x1080转640x640整体预处理耗时还是有保障的因为单帧RGA处理只需要1毫秒左右4路的排队任务积累也不明显。但要注意一个调度策略不要为每路摄像头开一个线程去频繁调用RGA这样线程切换和锁竞争反而增加开销。更好的做法是采集线程和推理线程分离采集线程负责把帧送入队列推理线程从队列取帧后统一走RGA预处理再交给NPU。这样RGA的调用是串行且有序的内核排队次数少端到端的帧率更稳定。4.4 带裁切的预处理YOLOv8的letterboxYOLOv8官方预处理通常做letterbox把原始图像等比缩放到640x640内部四周填充灰色。之前的做法是用OpenCV的resize加copyMakeBorder这两个操作在CPU上都有不少耗时。换成RGA后可以分两步走第一步用imresize把NV12缩放到一个等比尺寸比如从1920x1080缩放到640x360。第二步构造一个640x640的目标buffer用imfill填充灰色再用RGA的imblend或者直接经RGA裁剪混合把缩放后的图像放到目标区域。严格来说一次RGA操作做不了带填充的letterbox因为letterbox包含两张图的操作。但在实际项目中我发现很多时候不需要严格letterbox。如果你的模型训练时是按等比缩放填充的方式那可以把两步分开做性能仍然比CPU方案快很多如果模型可以接受直接拉伸缩放那干脆一次imresize搞定速度最快。5. 性能调优与避坑指南5.1 性能对照RGA vs CPU下面这组数据来自我在RK3588 Ubuntu系统上的实际测量操作内容是1920x1080 NV12转640x640 RGB888各跑100帧取平均。实现方式单帧耗时CPU占用估算备注OpenCV cvtColor resize15-25ms高依赖TBB/Neon优化不稳定纯C优化 NEON手写8-12ms较高开发成本高RGA imresize0.8-1.5ms几乎为0硬件加速稳定差距非常悬殊。这也是为什么我反复强调在RK3588上做视觉RGA不是可选项而是必选项。凡是涉及图像缩放、格式转换的操作都应该优先考虑用RGA去扛。5.2 常见问题排查速查表实际使用RGA的过程中几乎每个人都会碰到下面这几个问题。现象可能原因解决方法imresize返回IM_STATUS_FAILED格式组合不在硬件支持列表拆成两步先转格式再缩放或换一种格式组合输出图像花屏、斜条纹stride对齐没有设置使用wrapbuffer_virtualaddr_t传递正确的wstrideRGA调用不报错但画面全黑源buffer数据未就绪或未flush cache确保源数据完整使用dma_buf fd方式传入同一线程连续调用偶发失败RGA操作未完成就被修改buffer使用IM2D的同步接口等待返回后再操作buffer多线程调用时部分线程失败多个线程共享一个buffer每路线程使用独立buffer避免竞争编译找不到im2d.hlibrga版本太旧从源码重新编译新版librga/dev/rga不存在驱动未加载检查内核配置确认CONFIG_ROCKCHIP_RGA开启5.3 独家避坑经验第一个经验尽量用dma_buf fd而不是裸虚拟地址。虽然IM2D接口用虚拟地址简单方便但虚拟地址方式涉及RGA驱动的mmap和cache操作在高分辨率、高频调用时偶尔会出现内存同步问题。用dma_buf fd可以确保RGA访问和CPU访问之间的一致性更可控。在实际项目中如果你从摄像头驱动或者解码器拿到的本身就是dma_fd就直接传给wrapbuffer_fd。第二个经验RGA的格式支持列表不要背文档要实测。瑞芯微文档里会列一长串支持的格式和转换组合但不同版本的RGA硬件和支持列表有微调。我在项目里就遇到过文档说支持RGBA转NV12实际调用却失败的情况。建议拿到板子后写一个小测试程序把你要用的源格式到目标格式的每一种组合都跑一遍确保稳定后再写进正式代码。第三个经验小尺寸图像没必要用RGA。RGA的优势在分辨率高的场景如果你的图像只有320x240CPU处理也就1毫秒以内专门调RGA反而有函数调用和驱动开销性能提升不明显。我一般以512x512作为分界线大于这个尺寸才考虑RGA。第四个经验注意多次RGA操作之间的buffer生命周期。如果你用的是临时buffer比如malloc出来的虚拟地址RGA返回后就不能再动了如果复用同一个buffer下一次调用前要确保上一次操作已经完成。IM2D同步接口帮我们省了这一步但一旦改成异步模式或者多线程共享buffer这条特别容易出问题。6. 再往前一步RGA和USB摄像头RTSP推流的组合还有一个常见的需求是USB摄像头转RTSP流。USB摄像头插上RK3588v4l2节点拿到的是YUYV或者MJPEG推流编码前需要转成NV12同时统一分辨率。我之前写的处理链路是v4l2读帧 - RGA转NV12 - 设置编码器输入 - 硬编H.264/H.265 - 推RTSP。RGA在这个链路里承担的是格式转换和缩放整个推流过程CPU占用在单路场景下几乎为0。这里有个小技巧v4l2读出来的buffer通常是v4l2_buffer里面带长度和offset但不一定是连续dma buffer如果直接用虚拟地址转要注意V4L2的缓冲可能带有padding。我在代码里用mmap映射后读取了v4l2_format里的bytesperline换算成RGA的wstride问题就解决了。整条推流链路调通之后延迟大约在100到200毫秒之间画质清晰CPU占用不到5%这是纯软件转码方案完全做不到的。我个人在实际项目里的选择是所有涉及大尺寸图像格式转换和缩放的地方无脑切RGA只有小图操作和不可控的第三方库才保留CPU路径。这几个月下来踩过格式不对的坑也排过stride对齐的错但一旦把这些基础问题解决掉RGA带来的性能和稳定性收益是非常确定的。希望这篇实战记录能让你在RK3588上少走几步弯路。
返回列表