【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。
传统 Pipeline(CPU 前处理):
摄像头取流 → 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 能做什么:核心能力一览
RGA(Raster Graphic Acceleration)是瑞芯微的专用 2D 图像处理硬件,独立于 CPU/NPU/GPU,常见操作包括:
| 操作 | 典型场景 | CPU耗时(1080P) | RGA耗时 |
|---|---|---|---|
| Resize(缩放) | 适配模型输入尺寸 | 10-15ms | <1ms |
| Color Convert(格式转换) | YUV→RGB、BGR→RGB | 3-5ms | <0.5ms |
| Crop(裁剪) | ROI 区域提取 | 2-4ms | <0.3ms |
| Rotate(旋转) | 摄像头方向校正 | 5-8ms | <1ms |
| Flip(镜像) | 前置摄像头镜像 | 2-3ms | <0.3ms |
| Blend(图层叠加) | 检测框绘制到视频流 | 4-6ms | <0.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 ldconfig
3.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_t
RGA 操作的核心数据结构是 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_fd(DMA 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.3ms | 8ms | 20.3ms | 49 FPS |
| RGA预处理(虚拟地址) | 0.9ms | 8ms | 8.9ms | 112 FPS |
| RGA预处理 + DMA零拷贝 | 0.6ms | 8ms | 8.6ms | 116 FPS |
| RGA + DMA + 三核NPU并发 | 0.6ms | 5ms | 5.6ms | 178 FPS |
结论:单纯引入 RGA 就能把帧率从 49 FPS 提升到 112 FPS,是性价比最高的单项优化。叠加 DMA 零拷贝和三核并发(第7篇),可以再提升 60%。
九、常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
imresize 返回失败 |
输入输出格式不匹配 | 检查 RK_FORMAT_* 是否对应实际数据格式 |
| RGA 处理后图像花屏 | stride(行对齐)未正确设置 | 部分摄像头输出有内存对齐要求,用 wrapbuffer_virtualaddr_t 显式指定 stride |
| DMA 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篇
更多推荐

所有评论(0)