一、前言
我之前做过一个 RK3588 开发板部署 YOLO 算法的一个项目,该项目的源码已公开,并且有专门的文章做讲解:
本项目的视频数据处理流程始于本地 MP4 视频文件,依次经过以下核心环节:
-
FFmpeg 硬件解码 :利用 Rockchip 多媒体处理平台 RKmpp进行硬件加速解码,高效获取视频帧。
-
AVFrame 数据结构 (NV12 格式):解码后数据暂存于 FFmpeg 的 AVFrame 结构中,保持 NV12 色彩格式。
-
CV::Mat 数据结构 (NV12 格式):将 AVFrame 数据转换并封装至 OpenCV 的 Mat 结构中,便于后续图像操作。
-
RGA 硬件转换 :通过 Rockchip RGA 硬件模块加速图像色彩空间与格式转换。
-
CV::Mat 数据结构 (BGR 格式):最终获得 BGR 格式的 Mat 数据,为后续计算机视觉任务(如目标检测、图像识别)提供标准输入。
该流程充分利用硬件加速(RKmpp, RGA),显著提升了视频解码与格式转换的效率。
二、DRM Prime
考虑到数据在传输过程中,存在过多的数据拷贝操作,以及 CPU 需要全程监控数据的传输,因此想到了 DMA 来作为数据传输的媒介,提高传输效率。
FFmpeg 解码器编译支持 RKmpp 插件,支持输出 DRM Prime,也就是输出到一个 DMA Buf 中,这个过程不需要 CPU 参与。我本人也是第一次接触 DRM Prime,所以这次的目的仅限于加速解码,其他功能暂不讨论。
有关编译支持 RKmpp 硬件解码的 FFmpeg,可以查看我的另一篇文章:

可以看到,解码器的输出支持 NV12 等色彩空间的 AVFrame,并且支持输出到 DRM Prime。
三、修改编码器设置
1、修改初始化
在原来初始化解码器的基础上,增加输出格式,通过 AVDictionary 结构体设置,并由 avcodec_open2 接口进行写入:
cpp
#include <drm_fourcc.h>
/* 设置解码器输出为 DRM PRIME 格式 */
AVDictionary* codec_opts = nullptr;
av_dict_set(&codec_opts, "output_mode", "drm_prime", 0);
av_dict_set(&codec_opts, "output_format", "drm_prime", 0); // 可选但推荐
/* 打开编解码器,传递选项 */
if (avcodec_open2(codecContext, codec, &codec_opts) < 0) {
av_dict_free(&codec_opts);
throw std::runtime_error("Couldn't open decoder with DRM_PRIME");
}
av_dict_free(&codec_opts);
注意头文件,这个一般开发板都会有,路径在:
/usr/include/libdrm
2、修改取帧操作
在取帧时,增加两个检查的语句,确保输出格式设置成功,以及解码器编译成功:
cpp
// 检查解码输出格式
if (tempFrame->format != AV_PIX_FMT_DRM_PRIME) {
std::cout << "Format not DRM_PRIME: " << av_get_pix_fmt_name((AVPixelFormat)tempFrame->format) << std::endl;
break;
}
// 获取DRM描述符
AVDRMFrameDescriptor* desc = (AVDRMFrameDescriptor*)tempFrame->data[0];
// DMA-BUF文件描述符
int drm_prime_fd = desc->objects[0].fd;
uint32_t format = desc->layers[0].format;
// 检查帧格式是否为NV12
if (format != DRM_FORMAT_NV12) {
printf("Not NV12: Format = %c%c%c%c (0x%08X)\n",
(char)(format), (char)(format >> 8),
(char)(format >> 16), (char)(format >> 24),
format);
break;
}
首先是帧的格式,如果不使用 DRM,这里就是对应的色彩空间,例如 NV12;如果使用 DRM,这里的输出就是 AV_PIX_FMT_DRM_PRIME。
其次是 DRM 内部数据的色彩空间,使用 AVDRMFrameDescriptor 结构体来获取,然后与 DRM_FORMAT_NV12 进行对比,检查 DRM 内部是否为 NV12 格式。
这里的 drm_prime_fd 就是 DRM 对应的 DMA Buf 的文件描述符 ,可以直接送入 RGA 进行硬件转换 。我暂时没找到相关的官方示例,不过我觉得理论上没问题。从结果来看转换没有问题,并且速度极快。
3、传入 RGA
下面这个是 RK 官方的使用 DMA FD 调用 RGA 的demo,我在此基础上进行更改的:
cpp
/*
* Copyright (C) 2022 Rockchip Electronics Co., Ltd.
* Authors:
* YuQiaowei <cerf.yu@rock-chips.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_NDEBUG 0
#undef LOG_TAG
#define LOG_TAG "rga_cvtcolor_csc_demo"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <linux/stddef.h>
#include "RgaUtils.h"
#include "im2d.hpp"
#include "utils.h"
#include "dma_alloc.h"
#define LOCAL_FILE_PATH "/data"
int main() {
int ret = 0;
int src_width, src_height, src_format;
int dst_width, dst_height, dst_format;
int src_dma_fd, dst_dma_fd;
char *src_buf, *dst_buf;
int src_buf_size, dst_buf_size;
rga_buffer_t src_img, dst_img;
rga_buffer_handle_t src_handle, dst_handle;
memset(&src_img, 0, sizeof(src_img));
memset(&dst_img, 0, sizeof(dst_img));
src_width = 1280;
src_height = 720;
src_format = RK_FORMAT_RGBA_8888;
dst_width = 1280;
dst_height = 720;
dst_format = RK_FORMAT_YCbCr_420_SP;
src_buf_size = src_width * src_height * get_bpp_from_format(src_format);
dst_buf_size = dst_width * dst_height * get_bpp_from_format(dst_format);
ret = dma_buf_alloc(DMA_HEAP_DMA32_UNCACHED_PATH, src_buf_size, &src_dma_fd, (void **)&src_buf);
if (ret < 0) {
printf("alloc src dma_heap buffer failed!\n");
return -1;
}
ret = dma_buf_alloc(DMA_HEAP_DMA32_UNCACHED_PATH, dst_buf_size, &dst_dma_fd, (void **)&dst_buf);
if (ret < 0) {
printf("alloc dst dma_heap buffer failed!\n");
dma_buf_free(src_buf_size, &src_dma_fd, src_buf);
return -1;
}
/* fill image data */
if (0 != read_image_from_file(src_buf, LOCAL_FILE_PATH, src_width, src_height, src_format, 0)) {
printf("src image read err\n");
memset(src_buf, 0xaa, src_buf_size);
}
memset(dst_buf, 0x80, dst_buf_size);
src_handle = importbuffer_fd(src_dma_fd, src_buf_size);
dst_handle = importbuffer_fd(dst_dma_fd, dst_buf_size);
if (src_handle == 0 || dst_handle == 0) {
printf("importbuffer failed!\n");
goto release_buffer;
}
src_img = wrapbuffer_handle(src_handle, src_width, src_height, src_format);
dst_img = wrapbuffer_handle(dst_handle, dst_width, dst_height, dst_format);
imsetColorSpace(&src_img, IM_RGB_FULL);
imsetColorSpace(&dst_img, IM_YUV_BT709_LIMIT_RANGE);
ret = imcheck(src_img, dst_img, {}, {});
if (IM_STATUS_NOERROR != ret) {
printf("%d, check error! %s", __LINE__, imStrError((IM_STATUS)ret));
goto release_buffer;
}
ret = imcvtcolor(src_img, dst_img, src_format, dst_format);
if (ret == IM_STATUS_SUCCESS) {
printf("%s running success!\n", LOG_TAG);
} else {
printf("%s running failed, %s\n", LOG_TAG, imStrError((IM_STATUS)ret));
goto release_buffer;
}
write_image_to_file(dst_buf, LOCAL_FILE_PATH, dst_width, dst_height, dst_format, 0);
release_buffer:
if (src_handle)
releasebuffer_handle(src_handle);
if (dst_handle)
releasebuffer_handle(dst_handle);
dma_buf_free(dst_buf_size, &dst_dma_fd, dst_buf);
dma_buf_free(src_buf_size, &src_dma_fd, src_buf);
return ret;
}
由于 DRM 的输出本身就是 DMA,所以这里删除与输入图像有关的部分,也不需要申请原始图像的 DMA。
修改输出输出的尺寸,图像格式,准备各项数据,将刚刚得到的 drm_prime_fd 通过函数传参,直接传递给 importbuffer_fd 函数。
imsetColorSpace 函数我暂时还没有搞明白具体的用法,我尝试注释也能进行转换,这个好像是最近才增加的 API。
4、设置 RGA stride 填充信息
在 H264 编码中是以宏块为单位的,宏块的大小为 16*16,有的时候图像的宽度和高度不是16的整数倍,那么最右边会有一部分的长度在 1-15 之间,但是我们编码不能把这些数据丢掉,所以就需要对这些元素进行补齐 ,补齐之后的长度就叫做 stride ,所以正常情况下 stride >= 宽。

如果想对 FFmpeg 解码后的数据进行处理,就要考虑对齐的填充数据处理。
解决对齐的操作在我之前的项目中,是将图像存储的媒介由 AVFrame 转 CV::Mat 进行保存时处理的,因为 CV::Mat 的数据指针需要连续的颜色数据。
当前版本的 RGA 数据结构是支持自动解决 stride 对齐问题的,只需要在填充 rga_buffer_t 结构体时,设置 wstride变量即可,一般来叔高度不会填充,所以 hstride不需要设置,需要根据自身情况而定。
四、参考源码
下面是我根据官方的 demo 修改后的 API,读者自行参考:
cpp
int RGA_handle_nv12_to_rgb_fd_stride(int src_dma_fd, int dst_dma_fd,
int width, int height,
int src_hor_stride, int src_ver_stride) {
int ret = 0;
int src_width = width, src_height = height;
int dst_width = width, dst_height = height;
int src_format = RK_FORMAT_YCbCr_420_SP; // NV12
int dst_format = RK_FORMAT_RGB_888; // 3 通道 RGB
int src_buf_size, dst_buf_size;
rga_buffer_t src_img, dst_img;
rga_buffer_handle_t src_handle, dst_handle;
memset(&src_img, 0, sizeof(src_img));
memset(&dst_img, 0, sizeof(dst_img));
// 根据RGA文档,NV12格式需要计算正确的缓冲区大小
// Y平面: hor_stride * ver_stride
// UV平面: hor_stride * (ver_stride / 2)
src_buf_size = src_hor_stride * src_ver_stride * get_bpp_from_format(src_format);
// src_buf_size = src_width * src_height * get_bpp_from_format(src_format);
dst_buf_size = dst_width * dst_height * get_bpp_from_format(dst_format);
src_handle = importbuffer_fd(src_dma_fd, src_buf_size);
dst_handle = importbuffer_fd(dst_dma_fd, dst_buf_size);
if (src_handle == 0 || dst_handle == 0) {
printf("importbuffer failed!\n");
goto release_buffer;
}
// 使用wrapbuffer_handle封装缓冲区,并设置正确的stride
src_img = wrapbuffer_handle(src_handle, src_width, src_height, src_format);
dst_img = wrapbuffer_handle(dst_handle, dst_width, dst_height, dst_format);
// !!! 关键修改:设置源图像的实际步长 (stride) !!!
src_img.wstride = src_hor_stride; // 设置Y平面的水平步长(字节数)
src_img.hstride = src_ver_stride; // 设置垂直步长(总高度,包括填充行)
// 对于NV12格式,RGA会自动根据Y平面的stride和高度计算UV数据的位置
// UV数据通常紧跟在Y数据之后,且其水平步长与Y平面相同(hor_stride)
// 设置颜色空间(NV12 → RGB)
imsetColorSpace(&src_img, IM_YUV_BT709_LIMIT_RANGE);
imsetColorSpace(&dst_img, IM_RGB_FULL);
// 可选:添加参数检查(开发阶段建议开启)
/*ret = imcheck(src_img, dst_img, (im_rect){}, (im_rect){});
if (IM_STATUS_NOERROR != ret) {
printf("%d, check error! %s\n", __LINE__, imStrError((IM_STATUS)ret));
goto release_buffer;
}*/
// 执行颜色空间转换
ret = imcvtcolor(src_img, dst_img, src_format, dst_format);
if (ret != IM_STATUS_SUCCESS) {
printf("RGA conversion failed: %s\n", imStrError((IM_STATUS)ret));
ret = -1;
} else {
ret = 0;
}
release_buffer:
if (src_handle)
releasebuffer_handle(src_handle);
if (dst_handle)
releasebuffer_handle(dst_handle);
return ret;
}