系列导读:上一篇把 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_bufdst_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_EXPBUF ioctl)。


七、完整 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篇

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐