RKNN YOLOv5 推理直接使用 NV12 视频帧可行性分析
问题
从摄像头拉取的原始视频帧格式为 NV12 (YUV420SP),当前流程中需要先通过 nv12_to_rgb() 将 NV12 转换为 RGB888 后再送入 inference_yolov5_model() 做推理,造成不必要的 CPU 资源消耗。
核心结论
不能直接将 NV12 原始数据送入 YOLOv5 模型推理。
YOLOv5 模型的第一层卷积层期望输入为 3 通道 RGB 数据 (shape [1, 640, 640, 3]),而 NV12 是 YUV 色彩空间的 1.5 通道平面格式(Y 平面 + UV 交错平面),两者的数据布局和语义完全不同。即使使用 rknn_input.pass_through = 1 将原始字节直接传递给 NPU,模型也会输出无意义的结果。
最优方案:使用 Rockchip RGA(2D 硬件加速器)替代 CPU 完成 NV12→RGB888 转换。
该代码库的 image_utils.c 已原生支持此方案------通过 RGA 的 improcess() 在一次硬件调用 内完成 NV12→RGB888 色彩空间转换 + 缩放 + letterbox 填充,彻底消除 CPU 逐像素转换的开销。
代码库已有支持
关键结构体和函数已就位:
| 组件 | 位置 | 说明 |
|---|---|---|
IMAGE_FORMAT_YUV420SP_NV12 |
utils/common.h:13 |
image_buffer_t 支持的 NV12 格式枚举 |
get_rga_fmt() returns RK_FORMAT_YCbCr_420_SP |
utils/image_utils.c:468-469 |
NV12 到 RGA 格式的映射 |
convert_image_rga() |
utils/image_utils.c:498 |
使用 RGA 硬件转换,支持不同 src/dst 格式 |
convert_image_with_letterbox() |
utils/image_utils.c:699 |
NV12→RGB888 + resize + letterbox 一次完成 |
librga.so |
install/.../lib/librga.so |
已部署的 RGA 运行时库 |
代码修改方案
1. main.cc --- 替换 CPU 端 NV12→RGB 转换
删除以下代码:
cpp
// main.cc:1113 --- 分配 RGB 缓冲区
stream->rgb_buffer = (unsigned char *)malloc(WIDTH * HEIGHT * 3);
// main.cc:1123-1125 --- 设置 RGB888 格式
img.width = WIDTH;
img.height = HEIGHT;
img.format = IMAGE_FORMAT_RGB888;
// main.cc:1146-1147 --- CPU 转换并赋值
nv12_to_rgb((unsigned char *)frame, stream->rgb_buffer, WIDTH, HEIGHT);
img.virt_addr = stream->rgb_buffer;
替换为以下逻辑:
cpp
// 设置源图像:直接指向 NV12 帧
image_buffer_t src_img;
memset(&src_img, 0, sizeof(src_img));
src_img.virt_addr = (unsigned char *)frame;
src_img.width = WIDTH;
src_img.height = HEIGHT;
src_img.format = IMAGE_FORMAT_YUV420SP_NV12;
src_img.size = WIDTH * HEIGHT * 3 / 2; // NV12 size = w*h*1.5
// 设置目标图像:模型输入尺寸的 RGB888
image_buffer_t dst_img;
memset(&dst_img, 0, sizeof(dst_img));
dst_img.width = stream->model_ctx.model_width;
dst_img.height = stream->model_ctx.model_height;
dst_img.format = IMAGE_FORMAT_RGB888;
dst_img.size = stream->model_ctx.model_width * stream->model_ctx.model_height * 3;
dst_img.virt_addr = (unsigned char *)malloc(dst_img.size);
// RGA 硬件完成 NV12→RGB888 + resize + letterbox(一次调用)
letterbox_t letterbox;
convert_image_with_letterbox(&src_img, &dst_img, &letterbox, 114);
// 传给推理函数(格式 RGB888,尺寸匹配模型输入)
img = dst_img;
注意 :
dst_img.virt_addr需要在整个流生命周期内可复用(只需分配一次),建议将其存储在stream_runtime_t中,替换原有的rgb_buffer。
2. inference_yolov5_model() (rknpu2/yolov5.cc)
无需修改。
当前函数第 182-184 行会检查:
cpp
if (img->width == app_ctx->model_width &&
img->height == app_ctx->model_height &&
img->format == IMAGE_FORMAT_RGB888)
{
input_buf = img->virt_addr; // 走快路径,直接使用
}
由于 main.cc 已通过 RGA 将图像预处理为尺寸匹配的 RGB888,此快路径条件满足,函数会直接将缓冲区送入 NPU,无需任何额外的 CPU 转换。
3. convert.py --- 模型转换
无需修改。
RGA 硬件输出的 RGB888 数据与 ONNX/RKNN 模型期望的输入格式完全一致。
方案对比
| 方案 | CPU 负载 | 硬件加速 | 延迟 | 代码改动量 |
|---|---|---|---|---|
当前:CPU nv12_to_rgb() + 软件 resize |
高(逐像素) | 无 | 高 | 无 |
| RGA 硬件 NV12→RGB888 + resize | 极低 | RGA 硬件 | 极低(~1ms) | main.cc 少量修改 |
pass_through=1 直接送 NV12 |
无 | NPU | 低 | 需修改 ONNX 图,侵入性强 |
| 修改模型首层接受 NV12 | 无 | NPU | 低 | 需重新训练/修改模型,复杂度高 |
推荐方案:RGA 硬件加速转换------这是 Rockchip 平台的标准做法,代码改动最小,性能收益最大。
RGA 转换关键代码路径
main.cc
|
v
convert_image_with_letterbox(src=NV12, dst=RGB888)
|
v
convert_image()
|-- try: convert_image_rga() ← RGA 硬件 `improcess()`
| 成功 → NV12→RGB888 + resize + letterbox 一次完成
|
|-- fail fallback: convert_image_cpu()
`src->format != dst->format` → 返回 -1
(CPU 路径不支持跨格式转换,RGA 不可用时 NV12 路线不可用)
当前平台的 RGA 硬件至关重要。如果 RGA 不可用(如 RGA 驱动未加载),建议保持原有的 CPU
nv12_to_rgb()路径作为回退。