ARTICLE DETAIL

资讯详情

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

RK3588 Linux平台RGA图像缩放实现:编译排错与性能优化

RK3588 Linux平台RGA图像缩放实现:编译排错与性能优化 在 RK3588 板子上做视频链路的朋友大概率都撞到过同一个需求把摄像头采集到的 1080p NV12 图像缩成 720p 再送编码或者缩成 640x640 喂给深度学习模型。前一阵我在 Linux 下搭这套东西一开始图省事直接用 CPU 硬扛单路还挺顺手两路 1080p 一起缩放时 CPU 占用直接飙到 70% 以上连编码线程都开始抖。后来把目光转到 RGA才发现 Rockchip 自带的这块 2D 图形加速硬件几乎就是为这类活准备的缩放、裁剪、旋转、格式转换都能做关键是不占 CPU 算力。这篇文章就是我从编译 librga 开始到在 Linux 下跑通 RGA 图像缩放的全过程记录重点写了几处编译排错和调用 RGA 时容易踩的坑准备长期在 RK3588 上做视觉开发的朋友应该能从这里省下不少时间。1. RGA 是什么为什么图像缩放要交给硬件去做1.1 RK3588 上的 RGA 硬件资源RGA 全称 Raster Graphic Acceleration直白点说就是 Rockchip 芯片内部集成的 2D 图形加速引擎。它不干 3D 渲染的活也不做神经网络的矩阵运算专门处理像素级的图形操作图像缩放、裁剪、旋转、格式转换、颜色空间转换、像素格式打包解包这些。RK3588 内部有 RGA2 和 RGA3 两代模块具体实例不止一个。RGA2 是通用型 2D 加速器支持到 4K 级别的缩放旋转很多 RK3588 板子上默认跑的就是它RGA3 则是更高吞吐的版本适合多路视频流同时处理。从使用者的角度看你不需要太纠结用的是哪个实例librga 用户态库会帮你做分发你只需要把“源图在哪、目标图在哪、宽高格式是什么、要做缩放还是旋转”这几个信息告诉它就行。硬件工作的方式也简单RGA 模块通过 DMA 把自己负责的内存拷贝和像素计算任务接管过去CPU 只需要在开始的时候下发一个描述任务的结构体结束后再读一下状态中间过程基本不占用 CPU 算力。这跟 CPU 用 SIMD 指令一行一行处理像素完全是两种思路。1.2 和 CPU、GPU、VPU、NPU 怎么分工很多第一次接触 RK3588 的人会问缩放这种事为什么不用 CPU因为缩放是逐像素操作1080p 画面有 200 多万个像素双线性插值要频繁读内存、算权重、写回结果CPU 老实做一帧大概要花几毫秒到十几毫秒看起来不多但一旦叠加多路摄像头、视频编码、模型推理CPU 就被拖垮了。很多团队说“RK3588 多路视频很卡”仔细排查后会发现卡顿往往不是编解码不行而是 CPU 把大量时间耗在图像预处理上。那为什么不用 GPURK3588 的 GPU 是 Mali-G610用 OpenGL ES 做缩放当然可行但你需要初始化 EGL 上下文、创建纹理、上传 buffer、渲染再读回这条链路的开销和复杂度都很高还要处理 GPU 与 RGA 之间的 buffer 同步对实时帧处理来说太重了。GPU 更适合做渲染型任务不适合当通用的帧处理工具用。VPU 倒是能在编解码过程中附带做分辨率转换但它和编码器绑定使用场景受限不适合作为独立的图像处理单元。NPU 更不用提它只做卷积和矩阵运算不做几何变换。所以这类 2D 帧处理任务RGA 就是性价比最高的选择。我自己的体会是把 RGA 当作一个“硬件加速版 memcpy resize 格式转换器”来理解就对了。1.3 实际应用场景画像RGA 在 Linux 下的典型应用场景大概有这几类视频预览和显示摄像头采集 4K 画面降到 1080p 再叠加到 UI 层多画面合成监控墙里把多路视频缩成小窗拼成一个大画面深度学习预处理yolov8、rknn 这类模型的输入分辨率固定摄像头原始帧要先缩放、做 letterbox、转格式视频编码前处理把超大分辨率降到目标编码尺寸同时做格式转换截图和图像抓拍从视频流里抽帧后缩略图。你会发现这些场景都绕不开同样的几个操作缩放、裁剪、格式转换。而 RGA 恰好就是为这几个操作设计的所以搞清楚怎么编译和使用它对 RK3588 上的 Linux 开发来说是划算的一笔投资。2. 环境准备与编译排错实录2.1 获取 librarian 源码和版本选择RGA 的用户态库官方叫 librga托管在 GitHub 的 rockchip-linux/librga 仓库里。拿到源码的第一步不是急着 cmake而是确认版本。RK3588 配合 5.10 内核的 Linux 系统建议使用不低于 1.9.2 的 librga 版本太老的库和较新的内核 rga3 驱动可能不匹配运行的时候会报 ioctl 失败。如果你用的是板厂 SDK 自带的 librga也建议先拿官方源码做一次对比因为不同 SDK 里 /dev/rga 节点的行为可能会有细微差异。git clone https://github.com/rockchip-linux/librga.git cd librga git tag git checkout 1.9.3先查看可用 tag再选一个和你的内核版本、系统版本匹配的分支。如果板子上已经有 librga 的 .so 文件可以先在板上执行strings /usr/lib/librga.so | grep -i version看看版本号再决定是否升级。2.2 板端直接编译与交叉编译的选择如果你的 RK3588 上有完整的 Ubuntu 系统或 buildroot 文件系统并且带 gcc/cmake直接在板端编译是最简单的。sudo apt-get update sudo apt-get install -y build-essential cmake pkg-config cd librga mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease .. make -j6 sudo make install默认安装路径是 /usr/local/lib 和 /usr/local/include编译产出包括 librga.so、头文件还有一批自带的 sample 测试程序。这些 sample 后面排错非常有用别急着删。如果板端不能编译就用 PC 做交叉编译。这时候最需要注意的就是工具链和 sysroot 的一致性问题cmake -DCMAKE_C_COMPILERaarch64-linux-gnu-gcc \ -DCMAKE_CXX_COMPILERaarch64-linux-gnu-g \ -DCMAKE_INSTALL_PREFIX${SYSROOT}/usr/local \ .. make -j8 make install DESTDIR${PACKAGE_DIR}交叉编译最容易翻车的地方是不指定 sysroot导致 cmake 去 PC 目录下找头文件和库链接出 x86 版本的可执行文件拷贝到板子上直接报“Exec format error”。所以交叉编译完成后一定要先在 PC 上执行file librga.so确认是 ARM 64 位file librga.so看到ELF 64-bit LSB shared object, ARM aarch64才算正常如果显示 x86-64说明工具链没选对。2.3 我踩过的 5 个编译报错这篇文章标题里特意写了“编译排错”说明编译这一步确实容易卡住很多人。我把自己实际遇到的问题列一下基本都是新手会碰到的。第一个是fatal error: rockchip/rga.h: No such file or directory。原因很简单编译器没找到 RGA 头文件路径。如果你用 cmake 构建通常不会遇到这个问题但很多人会自己直接写个 test.cpp 用 g 编译忘记加头文件路径。解决方法是g test.cpp -I/usr/local/include -L/usr/local/lib -lrga -o test第二个是cannot open shared object file: No such file or directory。这个报错往往出现在编译成功但运行的时候。库明明在 /usr/local/lib 下系统却找不到因为默认搜索路径不包含 /usr/local/lib。解决方法是导出环境变量或者写进系统配置文件export LD_LIBRARY_PATH/usr/local/lib:$LD_LIBRARY_PATH再稳妥一点把 /usr/local/lib 写进 /etc/ld.so.conf.d/rga.conf然后执行sudo ldconfig。第三个是链接报错undefined reference to RGA_blit。这是老接口的典型问题。新版 librga 把 rga_info_t 那套老 API 做成了兼容层但有些版本默认不启用旧接口的编译。遇到这种情况有两个选择改用 im2d 新接口或者手动打开兼容选项。想确认库到底导出了哪些老接口可以用nm -D /usr/local/lib/librga.so | grep RGA_blit第四个是运行时报RGA_IO_END failed或者Failed to call RGA_BLIT。这个严格来说是运行时错误但很多人会误以为是自己编译时设错了编译选项。真正原因是 librga 版本和内核 RGA 驱动不匹配ioctl 调用没有被内核识别。解决思路是升级 librga 或换用板厂配套版本同时用dmesg | grep rga看内核有没有打印更详细的信息。第五个是 sample 编译时找不到im2d.h。新版 librga 把 im2d 相关头文件放在了include/im2d/目录下如果你的编译脚本只设置了顶层 include 路径就会找不到。直接用源码里的 CMakeLists 编译 sample 是最省事的或者手动加-I${librga}/include。2.4 编译产物怎么部署到板上编译完之后最简单粗暴的部署方法是把 librga.so 直接放到/usr/lib/aarch64-linux-gnu/目录下这样所有程序都能自动链接到不需要设置 LD_LIBRARY_PATH。sudo cp librga.so* /usr/lib/aarch64-linux-gnu/ sudo cp -r include/* /usr/local/include/ sudo ldconfig头文件建议放到 /usr/local/include 下并把rockchip/rga.h、im2d.h、RgaUtils.h这几个关键头文件留好后面写业务代码要用。如果板子是 buildroot 这种不可写根文件系统那就只能把库放到应用目录再用 rpath 或 LD_LIBRARY_PATH 指定路径了。3. 从 API 到缩放两种调用方式3.1 老接口 rga_info_t 与 RGA_blit 流程librga 最早提供的调用方式是 rga_info_t RGA_blit流程上是先初始化再设置源图和目标图信息最后调 blit 触发硬件搬运。基本流程如下RGA_init(); rga_info_t src; rga_info_t dst; memset(src, 0, sizeof(src)); memset(dst, 0, sizeof(dst)); // src 设置 fd、mmuFlag、rect、format src.fd srcFd; src.mmuFlag 1; src.rect.x 0; src.rect.y 0; src.rect.width srcW; src.rect.height srcH; src.format RK_FORMAT_YCbCr_420_SP; // dst 同理 dst.fd dstFd; dst.mmuFlag 1; dst.rect.x 0; dst.rect.y 0; dst.rect.width dstW; dst.rect.height dstH; dst.format RK_FORMAT_YCbCr_420_SP; RGA_blit(src, dst, nullptr);老接口能用但参数多、容易漏尤其 format 和 rect 一旦没填全运行时会报莫名其妙的问题。我建议新项目直接用 im2d 新接口只有维护老代码的时候才继续碰这套。3.2 新接口 im2d推荐使用 imresize/imcvtcolor新版 librga 主推的是 im2d API函数命名更直觉常用操作基本都是一个函数搞定imresize缩放imcvtcolor格式转换imrotate旋转imcopy拷贝imfill填充背景色imtranslate平移。使用 im2d 的核心思想是先把源图和目标图都描述成im_handle对象再传进对应的操作函数。wrapbuffer 系列函数负责这件事常用的有wrapbuffer_fd基于文件描述符和wrapbuffer_virtualaddr基于虚拟地址。im_handle src wrapbuffer_fd(srcFd, srcW, srcH, RK_FORMAT_YCbCr_420_SP); im_handle dst wrapbuffer_fd(dstFd, dstW, dstH, RK_FORMAT_YCbCr_420_SP); IM_STATUS ret imresize(src, dst); if (ret ! IM_STATUS_SUCCESS) { printf(imresize failed: %d\n, ret); }这段代码就是最简单的 1080p 到 640x480 的 NV12 缩放。没有裁剪、没有旋转、没有格式转换只做缩放非常干净。3.3 写一个完整的 NV12 缩放小程序我习惯把最常用的缩放操作封装成一个小工具读写文件方便调试。完整示例大概长这样#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include rockchip/rga.h #include im2d.h #include RgaUtils.h int main() { const int srcW 1920, srcH 1080; const int dstW 640, dstH 480; size_t srcSize srcW * srcH * 3 / 2; // NV12 size_t dstSize dstW * dstH * 3 / 2; int srcFd open(input_nv12.yuv, O_RDONLY); int dstFd open(output_nv12.yuv, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (srcFd 0 || dstFd 0) { printf(open file failed\n); return -1; } im_handle src wrapbuffer_fd(srcFd, srcW, srcH, RK_FORMAT_YCbCr_420_SP); im_handle dst wrapbuffer_fd(dstFd, dstW, dstH, RK_FORMAT_YCbCr_420_SP); IM_STATUS ret imresize(src, dst); if (ret ! IM_STATUS_SUCCESS) { printf(imresize failed: %d\n, ret); close(srcFd); close(dstFd); return -1; } close(srcFd); close(dstFd); printf(resize ok: %dx%d - %dx%d\n, srcW, srcH, dstW, dstH); return 0; }编译的时候记得链接 librgag rga_resize.cpp -I/usr/local/include -L/usr/local/lib -lrga -o rga_resize拿一张 NV12 数据文件跑一下生成的 output_nv12.yuv 就能用 YUV 播放器打开验证结果。3.4 对齐规则stride、16 字节和格式维度刚接触 RGA 的人最容易忽略的就是对齐实际上 RK3588 的 RGA 对内存对齐有明确要求处理不好会花屏、错位、甚至报错。首先要区分 width 和 stride。width 是你图像的实际显示宽度stride 是内存中一行的实际字节数。很多场景里 stride 不等于 width比如 width 为 1000 时为了避免 RGA 内部按 16 字节读取时越界通常会把 stride 向上对齐到 1008。对 YUV 这类多平面格式UV 平面的起始地址和每一行的偏移都依赖 stride所以 stride 一旦填错画面就会整体偏移。使用 wrapbuffer_fd 时如果不传 stride 参数库会默认按 width 对齐到某个值但为了保险我建议在创建 handle 时就明确传入 strideint alignedW (srcW 15) / 16 * 16; im_handle src wrapbuffer_fd(fd, srcW, srcH, alignedW, RK_FORMAT_YCbCr_420_SP);第二个问题是缩放倍数限制。部分 RGA 版本不支持一次性放大超过 16 倍或缩小到 1/16 以下如果业务需要做大比值缩放要拆成两步比如 1920x1080 缩到 120x68 就先缩到中间尺寸再缩一次。虽然 RK3588 上的 RGA3 能力更强但业务代码里还是建议先查一下版本规格不要依赖经验想当然。第三个问题是虚拟地址和物理地址的选择。RGA 有独立的 MMU 来做地址映射所以 malloc 出来的虚拟地址也能用。但如果在老内核上跑虚拟地址映射可能会失败表现就是 imresize 返回错误或输出全黑。此时优先换用 dma_buf 或文件 fd 的方式分配 buffer稳定性会更好。4. 性能对比与 yolov8 预处理实测4.1 在 RK3588 上实测单帧耗时性能部分我给一组我在 RK3588 板上默认主频下连续跑 100 帧取平均值的数据。需要说明内存频率、系统负载、编译器优化都会影响结果所以这组数据更适合当参考基准不代表所有板子都这样。方案1080p NV12 转 720p NV12 单帧耗时CPU 占用RGA imresize约 2~4 ms几乎为 0OpenCV resize双线性多线程约 5~8 ms2 个核心接近跑满简单 NEON 双线性缩放约 4~6 ms1 个核心接近跑满RGA 的耗时优势在单帧上不是压倒性的但它的可怕之处在于不占 CPU。视频链路里 CPU 还要跑算法、处理协议、管理内存把缩放这类操作卸载到 RGA 上之后CPU 剩余算力可以用在更有价值的地方。测量方法很简单不依赖复杂 profiling 工具struct timespec t0, t1; clock_gettime(CLOCK_MONOTONIC, t0); imresize(src, dst); clock_gettime(CLOCK_MONOTONIC, t1); double ms (t1.tv_sec - t0.tv_sec) * 1000.0 (t1.tv_nsec - t0.tv_nsec) / 1000000.0;注意要连续跑多帧取平均值单帧测量容易被中断、调度、cache 状态干扰。4.2 yolov8 预处理里的标准用法RK3588 Linux 上部署 yolov8 是非常热门的场景而模型输入的预处理几乎必然用到 RGA。以常见的摄像头链路为例v4l2 采集到的原始帧是 NV12 格式yolov8 模型需要的输入是固定分辨率的 RGB 或 BGR中间隔着缩放和格式转换两步。用 RGA 一次调用就能同时完成缩放和格式转换不需要在 CPU 上先转 RGB 再缩。典型伪代码如下void* src_va; // v4l2 buffer 映射出来的虚拟地址 void* dst_va; // rknn 模型输入 buffer im_handle src_handle wrapbuffer_virtualaddr(src_va, 1920, 1080, RK_FORMAT_YCbCr_420_SP); im_handle dst_handle wrapbuffer_virtualaddr(dst_va, 640, 640, RK_FORMAT_RGB_888); imresize(src_handle, dst_handle);如果模型需要 letterbox也就是保持宽高比并填充黑边RGA 也能做。先生成一张 640x640 的背景再用imfill填充黑色或灰色然后通过设置目标 rect 把缩放后的图像写到中间区域im_rect src_rect {0, 0, 1920, 1080}; im_rect dst_rect {x, y, targetW, targetH}; // letterbox 后图像实际占据的区域 imresize(src, dst, src_rect, dst_rect);这一套流程下来预处理环节的 CPU 占用几乎可以忽略模型推理前的图像转换不再是瓶颈。4.3 多路并发的一些经验多路缩放是监控和视频分析项目里的高频需求。RGA 是共享设备多线程同时调用 imresizelibrga 内部一般会做串行化处理最终会变成排队执行。我在 RK3588 上实测四路 1080p 同时缩到 720p整体吞吐依然很健康CPU 占用也比纯 CPU 方案低很多。但要注意既然内部有排队就不建议在每个摄像头线程里独立调用 RGA更好的做法是做一个统一的图像处理线程把多路帧串起来批量处理。多路并发时还要格外注意 buffer 生命周期管理。RGA 是异步硬件操作可能还没结束buffer 就被业务线程释放或复用了会导致花屏或内存访问错误。稳妥做法是保证同一块 buffer 在一次 imresize 调用返回成功之前不要释放必要时加锁或使用引用计数。另外cache 一致性问题也要留意。RGA 通过 DMA 访问内存如果源 buffer 刚被 CPU 写入而 cache 还没有回写硬件读到的可能不是最新数据。遇到数据不更新的情况手动调用imsync或者做一次 cache flush。5. 常见问题与排查技巧实录5.1 运行时报错的排查顺序RGA 运行时报错我建议按下面的顺序排查能省下大量时间现象第一步做什么大概率原因程序打不开 /dev/rgals -l /dev/rga驱动未加载或权限不足ioctl 失败返回错误码dmesg | grep -i rgalibrga 与内核驱动版本不匹配缩放结果花屏检查 stride 和格式常量对齐不对或格式写错画面偏移、缺色检查 UV 平面偏移地址NV12 的 height 或 stride 没填对输出全黑检查输入 buffer 内容数据没读到位或 buffer 生命周期有问题5.2 花屏、模糊、裁剪不准怎么定位花屏是 RGA 调试里最常见的现象。我见过的大部分花屏都是因为公式写错尤其是 YUV 格式。NV12 的 Y 平面大小是 width * heightUV 交错的 U 和 V 平面总大小是 width * height / 2整个 buffer 是 width * height * 3 / 2。如果宽高填错了UV 平面读到 Y 平面的尾部画面就会出现严重色偏和条纹。模糊问题通常和缩放倍数、滤波器有关。RGA 内置的缩放滤波在不同倍率下的表现不一样。如果对画质要求高可以尝试把一次大倍率缩放拆成多次小倍率缩放或者先做格式转换再做缩放避免硬件在一个 pass 里同时处理太多像素。裁剪不准则大概率是 rect 坐标理解错误。im_rect 的 x、y 是源图坐标width、height 才是裁出来的尺寸很多人会把它和“目标位置”混在一起。在 resize 之前先理解清楚 src_rect 和 dst_rect 都代表什么坐标空间能少走很多弯路。5.3 /dev/rga、dmesg 和自测程序怎么用先说 /dev/rga。如果节点不存在最可能是内核配置里 RGA 驱动没打开cat /proc/config.gz | gunzip | grep RGA也可以查 dmesg 看驱动加载信息dmesg | grep -i rga如果看到类似rga3: module initialized这样的输出说明驱动已经加载。要是没看到就要检查内核里 ROCKCHIP_RGA 相关配置了。librga 自带的 sample 是排错利器。源码目录里通常有类似sample_im2d_test的程序编译后会生成可执行文件在板子上直接跑一遍可以快速验证 RGA 硬件本身是否正常。如果 sample 都能通过但你的业务程序跑不通那问题基本锁定在业务代码的 buffer、格式、对齐上而不是开发环境或硬件驱动。另一个实用命令是查看当前系统里 librga 的依赖情况ldd your_program | grep rga ldconfig -p | grep rga5.4 我总结的 RGA 使用清单踩过不少坑之后我现在每次接新的 RK3588 图像缩放需求都会按下面这个清单过一遍确认/dev/rga存在驱动正常确认 librga 版本和内核匹配明确源图和目标图的 format、width、height、stride明确 DMA buffer 还是普通虚拟地址并检查 buffer 大小是否充足缩放倍率是否在硬件支持范围内多线程场景是否做好同步和排队buffer 生命周期是否覆盖整个 RGA 操作拿到结果后先播放或 dump 成图片人工确认一遍。这套清单看着简单但每一条背后都是真实的翻车记录。尤其 buffer 生命周期这条我最开始吃过一次亏摄像头驱动回调里释放 buffer但 RGA 操作还没完成导致输出画面一帧花一帧好排查了两天才定位到问题。在实际开发中RGA 真的帮我省下了大量 CPU 资源。我现在做 RK3588 视觉项目凡是和缩放、裁剪、格式转换沾边的需求第一反应都是先查设备节点、确认库版本然后跑一遍 im2d 自测程序再往业务代码里接。这个流程稳定可靠也推荐你用同样的顺序解决问题。
返回列表