【RK3588S 嵌入式AI系列⑧】RGA零拷贝图像加速:1080P预处理从12ms压到1ms内

📅 2026/7/1 3:37:34
【RK3588S 嵌入式AI系列⑧】RGA零拷贝图像加速:1080P预处理从12ms压到1ms内
系列导读上一篇把 NPU 三核算力榨干了但如果前处理resize、格式转换还在用 CPU 软件实现整条 Pipeline 的瓶颈就转移到了 CPU 上。这一篇把图像前处理全部卸载到 RGA 硬件加速器并打通零拷贝内存共享让 CPU 和 NPU 都从繁重的图像搬运工作中解放出来。一、为什么 CPU 软件预处理是隐形瓶颈回顾第一篇的实测数据用 OpenCV CPU 做一次 1080P→640×640 的 resize BGR2RGB耗时约 12ms。而 YOLOv8n 在 NPU 上单次推理只要 8ms。传统 PipelineCPU 前处理 摄像头取流 → CPU resize(12ms) → CPU 格式转换(3ms) → NPU推理(8ms) → 后处理 单帧总耗时约 23ms 最大帧率约 43 FPS RGA 加速 Pipeline 摄像头取流 → RGA resize转换(1ms) → NPU推理(8ms) → 后处理 单帧总耗时约 9ms 最大帧率约 110 FPS前处理占比从 65% 降到 11%这就是为什么 RGA 是 RK3588S 上性价比最高的优化手段——不需要改模型、不需要重新量化只是换了个执行图像操作的硬件单元。二、RGA 能做什么核心能力一览RGARaster Graphic Acceleration是瑞芯微的专用 2D 图像处理硬件独立于 CPU/NPU/GPU常见操作包括操作典型场景CPU耗时1080PRGA耗时Resize缩放适配模型输入尺寸10-15ms1msColor Convert格式转换YUV→RGB、BGR→RGB3-5ms0.5msCrop裁剪ROI 区域提取2-4ms0.3msRotate旋转摄像头方向校正5-8ms1msFlip镜像前置摄像头镜像2-3ms0.3msBlend图层叠加检测框绘制到视频流4-6ms0.5ms这些操作 RGA 都支持组合调用——一次 RGA 调用可以同时做 resize格式转换裁剪进一步减少调用开销。三、安装 RGA 库与环境准备3.1 获取 librga# 从瑞芯微官方仓库克隆 git clone https://github.com/airockchip/librga.git ~/librga cd ~/librga # 查看目录结构 ls # include/ 头文件 # libs/ 预编译库含 aarch64 版本 # samples/ 官方示例代码建议通读3.2 推送运行时库到板子adb push ~/librga/libs/AndroidNdk/arm64-v8a/librga.so /usr/lib/ # 或 Linux 版本路径可能是 adb push ~/librga/libs/Linux/aarch64/librga.so /usr/lib/ adb shell ldconfig3.3 CMake 配置# CMakeLists.txt 追加 set(RGA_PATH $ENV{HOME}/librga) target_include_directories(your_target PRIVATE ${RGA_PATH}/include ) target_link_directories(your_target PRIVATE ${RGA_PATH}/libs/Linux/aarch64 ) target_link_libraries(your_target rga )四、RGA基础用法Resize 格式转换4.1 核心概念rga_buffer_tRGA 操作的核心数据结构是rga_buffer_t描述一块图像缓冲区的格式信息#include RgaApi.h #include im2d.hpp // 创建一个 rga_buffer_t 描述符 rga_buffer_t wrap_buffer(void* buf, int width, int height, int format) { return wrapbuffer_virtualaddr((void*)buf, width, height, format); }4.2 完整 Resize 格式转换示例// rga_preprocess.cpp #include RgaApi.h #include im2d.hpp #include stdio.h #include cstdlib // 将 1080P NV12摄像头常见格式转换为 640×640 RGB模型输入格式 int rga_preprocess( unsigned char* src_buf, int src_w, int src_h, // 输入原始帧 unsigned char* dst_buf, int dst_w, int dst_h // 输出模型输入尺寸 ) { // ── 封装输入输出缓冲区描述符 ── rga_buffer_t src wrapbuffer_virtualaddr( src_buf, src_w, src_h, RK_FORMAT_YCbCr_420_SP // NV12 格式 ); rga_buffer_t dst wrapbuffer_virtualaddr( dst_buf, dst_w, dst_h, RK_FORMAT_RGB_888 // RGB888 格式 ); // ── 一次调用完成 resize 格式转换 ── IM_STATUS status imresize(src, dst); if (status ! IM_STATUS_SUCCESS) { fprintf(stderr, RGA resize 失败: %s\n, imStrError(status)); return -1; } return 0; } int main() { int src_w 1920, src_h 1080; int dst_w 640, dst_h 640; unsigned char* src_buf (unsigned char*)malloc(src_w * src_h * 3 / 2); // NV12 unsigned char* dst_buf (unsigned char*)malloc(dst_w * dst_h * 3); // RGB888 // ... 填充 src_buf从摄像头/解码器获取... struct timespec t0, t1; clock_gettime(CLOCK_MONOTONIC, t0); rga_preprocess(src_buf, src_w, src_h, dst_buf, dst_w, dst_h); clock_gettime(CLOCK_MONOTONIC, t1); float ms (t1.tv_sec-t0.tv_sec)*1000.0f (t1.tv_nsec-t0.tv_nsec)/1e6f; printf(RGA 预处理耗时: %.3f ms\n, ms); free(src_buf); free(dst_buf); return 0; }实测输出RGA 预处理耗时: 0.876 ms对比 CPU OpenCV 实现的 12ms提升超过 13 倍。五、组合操作Crop Resize Rotate 一次完成实际场景经常需要多个操作组合比如先裁剪 ROI 区域再缩放RGA 支持在一次调用中完成// rga_combo.cpp #include RgaApi.h #include im2d.hpp int rga_crop_resize_rotate( unsigned char* src_buf, int src_w, int src_h, int crop_x, int crop_y, int crop_w, int crop_h, // 裁剪区域 unsigned char* dst_buf, int dst_w, int dst_h, int rotate_angle // 0/90/180/270 ) { rga_buffer_t src wrapbuffer_virtualaddr( src_buf, src_w, src_h, RK_FORMAT_YCbCr_420_SP ); rga_buffer_t dst wrapbuffer_virtualaddr( dst_buf, dst_w, dst_h, RK_FORMAT_RGB_888 ); // 定义裁剪区域 im_rect src_rect {crop_x, crop_y, crop_w, crop_h}; im_rect dst_rect {0, 0, dst_w, dst_h}; // 旋转参数 int rotate_mode IM_HAL_TRANSFORM_ROT_0; if (rotate_angle 90) rotate_mode IM_HAL_TRANSFORM_ROT_90; if (rotate_angle 180) rotate_mode IM_HAL_TRANSFORM_ROT_180; if (rotate_angle 270) rotate_mode IM_HAL_TRANSFORM_ROT_270; // ── 一次调用裁剪 缩放 旋转 格式转换 ── IM_STATUS status improcess( src, dst, {}, // 空的 pat不需要叠加图层 src_rect, dst_rect, {}, rotate_mode ); return (status IM_STATUS_SUCCESS) ? 0 : -1; }人脸识别场景的典型应用检测模型先定位出人脸 ROI 坐标RGA 直接从原始大图裁剪出该区域并缩放到识别模型需要的尺寸如112×112全程不需要 CPU 参与裁剪缩放一次 RGA 调用在 1ms 内完成。六、零拷贝DMA Buffer 共享内存6.1 为什么需要零拷贝上面的例子里src_buf和dst_buf都是普通的malloc分配的虚拟地址内存。RGA硬件实际操作时仍然需要通过内存控制器读写——但如果上游摄像头/VPU解码器和下游RGA/NPU使用同一块物理内存DMA Buffer就能完全避免 CPU 介入的数据搬运。传统方式有拷贝 摄像头 DMA → CPU内存A → memcpy → CPU内存B(RGA输入) → RGA处理 → CPU内存C(NPU输入) 每次 memcpy 都消耗 CPU 周期和内存带宽 零拷贝方式 摄像头 DMA Buffer → RGA直接读取该Buffer → 输出到另一DMA Buffer → NPU直接读取 全程没有 CPU memcpy只有硬件间的 DMA 传输6.2 使用 DMA Buffer 的 RGA 调用// dma_buffer_rga.cpp #include RgaApi.h #include im2d.hpp #include sys/mman.h // 假设已经从 V4L2/解码器获取了 dma_fdDMA buffer 文件描述符 int rga_process_dma( int src_dma_fd, int src_w, int src_h, int dst_dma_fd, int dst_w, int dst_h ) { // ── 用 DMA fd 而非虚拟地址封装缓冲区 ── rga_buffer_t src wrapbuffer_fd( src_dma_fd, src_w, src_h, RK_FORMAT_YCbCr_420_SP ); rga_buffer_t dst wrapbuffer_fd( dst_dma_fd, dst_w, dst_h, RK_FORMAT_RGB_888 ); IM_STATUS status imresize(src, dst); return (status IM_STATUS_SUCCESS) ? 0 : -1; }6.3 RKNN 推理直接使用 DMA Buffer 输入RKNN SDK 同样支持直接从 DMA fd 读取输入跳过最后一次 memcpy// rknn_dma_input.cpp rknn_input inputs[1] {0}; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].fmt RKNN_TENSOR_NHWC; // ── 关键使用 DMA fd 而非 CPU 指针 ── inputs[0].buf nullptr; inputs[0].dma_buf_fd dst_dma_fd; // 直接复用 RGA 输出的 DMA buffer inputs[0].size dst_w * dst_h * 3; inputs[0].pass_through 0; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL);⚠️DMA Buffer 获取依赖具体的摄像头驱动V4L2或解码器MPP接口不同板厂的 BSP 实现细节略有差异建议参考你所用开发板官方提供的摄像头采集示例代码关注其中 DMA buffer 的导出方式通常通过VIDIOC_EXPBUFioctl。七、完整 Pipeline 示例摄像头→RGA→NPU整合前面的内容构建一条完整的零拷贝视频分析 Pipeline// full_pipeline.cpp #include RgaApi.h #include im2d.hpp #include rknn_api.h struct PipelineContext { rknn_context rknn_ctx; int model_input_w, model_input_h; }; // 单帧处理摄像头帧 → RGA预处理 → NPU推理 int process_frame(PipelineContext* pctx, unsigned char* camera_frame, int cam_w, int cam_h) { // ── Step1: RGA 预处理resize 格式转换── static unsigned char* model_input_buf (unsigned char*)malloc(pctx-model_input_w * pctx-model_input_h * 3); rga_buffer_t src wrapbuffer_virtualaddr( camera_frame, cam_w, cam_h, RK_FORMAT_YCbCr_420_SP ); rga_buffer_t dst wrapbuffer_virtualaddr( model_input_buf, pctx-model_input_w, pctx-model_input_h, RK_FORMAT_RGB_888 ); imresize(src, dst); // ── Step2: NPU 推理直接吃 RGA 输出── rknn_input inputs[1] {0}; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size pctx-model_input_w * pctx-model_input_h * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf model_input_buf; inputs[0].pass_through 0; rknn_inputs_set(pctx-rknn_ctx, 1, inputs); rknn_run(pctx-rknn_ctx, NULL); rknn_output outputs[1] {0}; outputs[0].want_float 1; rknn_outputs_get(pctx-rknn_ctx, 1, outputs, NULL); // ... 处理推理结果 ... rknn_outputs_release(pctx-rknn_ctx, 1, outputs); return 0; }八、性能实测对比以 1080P 摄像头输入、YOLOv8n 检测模型为基准Pipeline 方案预处理耗时推理耗时单帧总耗时理论最大帧率CPU OpenCV 预处理12.3ms8ms20.3ms49 FPSRGA预处理虚拟地址0.9ms8ms8.9ms112 FPSRGA预处理 DMA零拷贝0.6ms8ms8.6ms116 FPSRGA DMA 三核NPU并发0.6ms5ms5.6ms178 FPS结论单纯引入 RGA 就能把帧率从 49 FPS 提升到 112 FPS是性价比最高的单项优化。叠加 DMA 零拷贝和三核并发第7篇可以再提升 60%。九、常见问题排查问题原因解决方案imresize返回失败输入输出格式不匹配检查RK_FORMAT_*是否对应实际数据格式RGA 处理后图像花屏stride行对齐未正确设置部分摄像头输出有内存对齐要求用wrapbuffer_virtualaddr_t显式指定 strideDMA fd 调用报错fd 权限或生命周期问题确认 DMA buffer 在 RGA 处理完成前未被释放找不到librga.so库未推送到板子adb push librga.so /usr/lib/ ldconfig旋转后图像方向不对rotate_mode 枚举值用错核对IM_HAL_TRANSFORM_ROT_*对应的角度定义十、总结与下篇预告本篇把图像前处理的瓶颈彻底解决理解了 RGA 相比 CPU 软件实现的性能优势10倍以上掌握了 resize、格式转换、裁剪、旋转的组合调用打通了 DMA Buffer 零拷贝链路从摄像头到 NPU 全程硬件直通实测验证1080P Pipeline 帧率从 49 FPS 提升到 178 FPS至此前处理推理这条核心链路的性能优化全部完成。下一篇系列第 9 篇讲NPU 性能 Profiling 方法论如何用rknn_query拿到逐算子耗时数据精确判断瓶颈是计算受限还是带宽受限为更深度的优化提供数据支撑。本系列文章列表持续更新✅ 第1-7篇已发布点击专栏查看✅ 第8篇RGA零拷贝图像加速本文 第9篇NPU性能Profiling方法论… 共16篇