在 RK3588 多线程推理 YOLO 时,同时开启硬件解码和 RGA 加速的性能分析

news2025/4/3 0:09:13

一、前言

        本文是基于RK3588的YOLO多线程推理多级硬件加速引擎框架设计项目的延申与拓展,单独分析所提出的方案4的性能和加速原理,即同时开启 RKmpp 硬件视频解码和 RGA 硬件图像缩放、旋转。

二、实验结果回顾

        在项目的总览篇中,给出了该方案的推理效果,这里重新贴上:

         这个组合方式本应该是最快的方案,但是同时开启硬件解码和 RGA 加速,帧率就下降:

        使用 15 个推理线程(继续增加没有明显变化),帧数稳定在 139 帧左右,反倒是等于没有使用硬件加速,与纯 OpenCV 实现的方案结果相近。

        不过好消息是,相比于第一个方案,CPU 和 NPU 的占用率都有下降,CPU 占用率下降了约 21 个百分点,NPU 下降约 8 个百分点,这说明方案4的上限还没到,还有优化的空间。下面是 RGA 的使用情况,使用率与之前保持相同:

三、代码逻辑分析

1、加载器

        方案4的视频加载器是 FFmpeg,并且已经提前编译好源码,支持 RKmpp 解码器,只需要在初始化时选择 h264_rkmpp 即可,其他流程就是普通的 FFmpeg C++接口配置方法,具体详见下面的文章:

2、取出图像

        主函数的 for 循环每次取出一帧,然后传递给线程池,由于实操过程发现前几帧总是非正常图像,于是做了一个跳过处理:

/* 跳过不需要的帧 */
if (!video_reader_ptr->readFrame(img)) {
    // 如果已经成功开始处理图像,则代表已处理完所有图像或读取错误
    if (fps != 0)
        break;
    else
        continue;
    }

// 放入 rknn 线程池
if (!img.empty())
    if (yolo_pool.put(img) != 0)
        break;

        video_reader_ptr 是 FFmpeg 和 OpenCV 的多态指针,在执行程序时根据命令行参数选择具体的实例化类,在方案 4 中,使用的是 FFmpeg,调用 RKmpp。下面是 FFmpeg 类的 readFrame 函数:

/**
 * @Description: 读取一帧
 * @param {Mat&} frame: 取出的帧
 * @return {*}
 */
bool FFmpegReader::readFrame(cv::Mat& frame) {
    // 读取帧
    /*if (av_read_frame(formatContext, packet) < 0) {
        return false; // 没有更多帧
    }*/
    while (av_read_frame(formatContext, packet) >= 0) {
        if (packet->stream_index != videoStreamIndex) {
            av_packet_unref(packet);
            continue;
        }
        break;
    }

    // 如果是视频流
    if (packet->stream_index != videoStreamIndex) {
        cerr << "Not a video stream: " << packet->stream_index << " != " << videoStreamIndex << endl;
        av_packet_unref(packet);
        return false; // 不是视频流
    }

    // 发送数据包到解码器
    if (avcodec_send_packet(codecContext, packet) < 0) {
        std::cerr << "Failed to send packet to decoder" << std::endl;
        av_packet_unref(packet);
        return false; // 发送数据包失败
    }

    // 接收解码后的帧
    if (avcodec_receive_frame(codecContext, tempFrame) < 0) {
        std::cerr << "Failed to receive frame from decoder" << std::endl;
        av_packet_unref(packet);
        return false;
    }

    // 成功读取一帧,保存在 tempFrame 中
    // 将帧数据转换为 cv::Mat BGR 格式
    if (this->NV12_to_BGR(frame) != 0) {
        std::cerr << "Failed to convert YUV420SP to BGR" << std::endl;
        av_packet_unref(packet);
        return false;
    }

    // 释放数据包
    av_packet_unref(packet);
    
    return true; // 处理完成
}

        这里的 av_read_frame 和上面逻辑一样,目的是跳过部分帧,我实测发现会在视频开始的第一次需要跳过,正常播放后就不会再进入循环。需要关注的是 NV12_to_BGR 函数,它是我用于转换图像格式的接口:

/**
 * @Description: 转换格式,NV12 转 BGR
 *               该函数内有三种转换方式:
 *                  1. FFmpeg SwsContext 软件转换  
 *                  2. OpenCV 软件转换,可启用 opencl(目前区别不大)
 *                  3. RGA 硬件加速转换
 * @param {Mat&} frame: 
 * @return {*}
 */
int FFmpegReader::NV12_to_BGR(cv::Mat& bgr_frame) {
    if (tempFrame->format != AV_PIX_FMT_NV12) {
        return -EXIT_FAILURE; // 格式错误
    }

    // 设置输出帧的尺寸和格式,防止地址无法访问
    bgr_frame.create(tempFrame->height, tempFrame->width, CV_8UC3);

#if 0 
    // 方式1:使用 FFmpeg SwsContext 软件转换
    return this->FFmpeg_yuv420sp_to_bgr(bgr_frame);
#endif

    // 创建一个完整的 NV12 数据块(Y + UV 交错)
    cv::Mat nv12_mat(tempFrame->height + tempFrame->height / 2, tempFrame->width, CV_8UC1);
    // 将 AVFrame 内的数据,转换为 OpenCV Mat 格式保存
    this->AV_Frame_To_CVMat(nv12_mat);

    // 硬件加速
    if (this->accels_2d == ACCELS_2D::ACC_OPENCV) {
        // 方式2:使用 OpenCV 软件转换
        cv::cvtColor(nv12_mat, bgr_frame, cv::COLOR_YUV2BGR_NV12);
        return EXIT_SUCCESS;
    } else if (this->accels_2d == ACCELS_2D::ACC_RGA) {
        // 方式3:使用 RGA 硬件加速转换
        return RGA_yuv420sp_to_bgr((uint8_t *)nv12_mat.data, tempFrame->width, tempFrame->height, bgr_frame);
    }
    else
        return -EXIT_FAILURE;
}

        具体的细节在注释中都有说明,有三种转换方式,分别是

1. FFmpeg SwsContext 软件转换  

2. OpenCV 软件转换

3. RGA 硬件加速转换

        方案 4 使用 RGA 进行转换。

3、色彩空间转换

       取出的原始帧格式为 RK_FORMAT_YCbCr_420_SP(NV12),由于 YOLO 的输入格式需要 RGB888,因此需要使用 RGA 的 imcvtcolor 接口进行一次图像色彩空间的转换,所以针对 NV12 转 RGB888 编写了一个模块,数据的传输依赖 cv::Mat 对象:

/**
 * @Description: 将 YUV420SP(NV12) 格式的图像转换为 BGR 格式
 * @param {uint8_t} *frame_yuv_data: 原始 YUV 图像数据(这里为了减少对 AVFrame 的依赖,只传入 data 数据)
 * @param {Mat} &bgr_image: 转换后的 BGR 图像
 * @return {*}
 */
int RGA_yuv420sp_to_bgr(const uint8_t *frame_yuv_data, const int& width, const int& height, cv::Mat &bgr_image) {
    rga_buffer_t src_img, dst_img;
    rga_buffer_handle_t src_handle, dst_handle;
    int src_width, src_height, src_format;
    int dst_width, dst_height, dst_format;
    int src_buf_size, dst_buf_size;
    memset(&src_img, 0, sizeof(src_img));
    memset(&dst_img, 0, sizeof(dst_img));

    /* 输入输出大小 */ 
    src_width = width;
    src_height = height;
    dst_width = bgr_image.cols;
    dst_height = bgr_image.rows;

    /* 将转换前后的格式 */
    // 选自 rk 官方 demo 中的格式
    src_format = RK_FORMAT_YCbCr_420_SP;
    dst_format = RK_FORMAT_BGR_888;

    /* 使用图像数据构建 rga_buffer_t 结构体,两个指针指向同一个数据 */
    src_img = wrapbuffer_virtualaddr((void *)frame_yuv_data, src_width, src_height, src_format);
    dst_img = wrapbuffer_virtualaddr((void *)bgr_image.data, dst_width, dst_height, dst_format);

    /* 图像检查 */
    IM_STATUS STATUS;
    /*STATUS = imcheck(src_img, dst_img, {}, {});
    if (IM_STATUS_NOERROR != STATUS) {
        printf("%d, check error! %s", __LINE__, imStrError(STATUS));
        return -1;
    }*/

    /* 将需要转换的格式与rga_buffer_t格式的结构体src、dst⼀同传⼊imcvtcolor() */
    STATUS = imcvtcolor(src_img, dst_img, src_format, dst_format);

    /* 释放内存(正确和错误均执行) */
    if (src_handle)
        releasebuffer_handle(src_handle);
    if (dst_handle)
        releasebuffer_handle(dst_handle);

    if (IM_STATUS_SUCCESS != STATUS) {
        fprintf(stderr, "rga resize error! %s", imStrError(STATUS));
        return -1;
    }
    return 0;
}


/**
 * @Description: 将 YUV420SP(NV12) 格式的图像转换为 BGR 格式
 * @param {uint8_t} *frame_yuv_data: 原始 YUV 图像数据(这里为了减少对 AVFrame 的依赖,只传入 data 数据)
 * @param {Mat} &bgr_image: 转换后的 BGR 图像
 * @return {*}
 */
int RGA_handle_yuv420sp_to_bgr(const uint8_t *frame_yuv_data, const int& width, const int& height, cv::Mat &bgr_image) {
    rga_buffer_t src_img, dst_img;
    rga_buffer_handle_t src_handle, dst_handle;
    int src_width, src_height, src_format;
    int dst_width, dst_height, dst_format;
    int src_buf_size, dst_buf_size;
    memset(&src_img, 0, sizeof(src_img));
    memset(&dst_img, 0, sizeof(dst_img));

    /* 输入输出大小 */ 
    src_width = width;
    src_height = height;
    dst_width = bgr_image.cols;
    dst_height = bgr_image.rows;

    /* 将转换前后的格式 */
    // 选自 rk 官方 demo 中的格式
    src_format = RK_FORMAT_YCbCr_420_SP;
    dst_format = RK_FORMAT_BGR_888;

    /* 计算图像所需要的 buffer 大小 */
    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);

    /* 将缓冲区对应的物理地址信息映射到RGA驱动内部,并获取缓冲区相应的地址信息 */
    src_handle = importbuffer_virtualaddr((void *)frame_yuv_data, src_buf_size);
    dst_handle = importbuffer_virtualaddr(bgr_image.data, dst_buf_size);

    /* 封装为RGA图像结构 */
    src_img = wrapbuffer_handle(src_handle, src_width, src_height, src_format);
    dst_img = wrapbuffer_handle(dst_handle, dst_width, dst_height, dst_format);

    /* 图像检查 */
    IM_STATUS STATUS;
    /*STATUS = imcheck(src_img, dst_img, {}, {});
    if (IM_STATUS_NOERROR != STATUS) {
        printf("%d, check error! %s", __LINE__, imStrError(STATUS));
        return -1;
    }*/

    /* 将需要转换的格式与rga_buffer_t格式的结构体src、dst⼀同传⼊imcvtcolor() */
    STATUS = imcvtcolor(src_img, dst_img, src_format, dst_format);

    /* 释放内存(正确和错误均执行) */
    if (src_handle)
        releasebuffer_handle(src_handle);
    if (dst_handle)
        releasebuffer_handle(dst_handle);

    if (IM_STATUS_SUCCESS != STATUS) {
        fprintf(stderr, "rga resize error! %s", imStrError(STATUS));
        return -1;
    }
    return 0;
}

        这里我放了两个函数,它们实现的是同一个功能,区别在于是否将数据导入 RGA 驱动内部。函数名带有 handle 代表使用 RGA 驱动内部进行数据处理,另一个函数则是直接操作用户提供的图像数据,详细的分析可以看这篇文章:

瑞芯微RKRGA(librga)Buffer API 分析-CSDN博客https://blog.csdn.net/plmm__/article/details/146571080?spm=1001.2014.3001.5501        在 YOLO 这样需要高频率取出图像并进行格式转换的场景,我自然选择直接操作原始的图像数据,我在实践后也确实如此,相比之下每帧的处理速度可以提高 2ms 左右(尺寸1024*720)。

4、适配模型输入

        另一个使用到 RGA 库的就是模型的推理部分,由于输入 YOLO 模型的图像色彩空间需要 RGB888,而调用 OpenCV 的 imshow() 函数则需要 cv::Mat 的默认格式 BGR888,除非不显示图像检测结果(那就失去了目标检测的灵魂)。

        原作者将图像的前处理和后处理都放在了推理函数中,目的就是可以将他们独立到各个线程中,提高多线程执行效率。由于后处理部分主要使用 BGR888 格式,因此推理函数中就需要同时存在这两种格式。本来想在解码时就直接转为 RGB888,但是后处理调试比较费时间,并且考虑到可以随时切换为 OpenCV 进行软件转换,提高代码的复用性,于是在推理线程中保留了格式转换的 RGA 接口:

// YOLO 推理需要 RGB 格式,后处理需要 BGR 格式
    // 即使前处理时提前转换为 RGB,后处理部分任然需要转换为 BGR,需要在本函数中保留两种格式
    if (this->config.accels_2d == ACCELS_2D::ACC_OPENCV) {
        cv::cvtColor(orig_img, rgb_img, cv::COLOR_BGR2RGB);
    }
    else if (this->config.accels_2d == ACCELS_2D::ACC_RGA) {
        if (RGA_bgr_to_rgb(orig_img, rgb_img) != 0) {
            cout << "RGA_bgr_to_rgb error" << endl;
            return cv::Mat();
        }
    }
    else {
        cout << "Unsupported 2D acceleration" << endl;
        return cv::Mat();
    }

        如果输入尺寸不匹配,还会进行 resize 操作:

 // 图像缩放
    if (orig_img.cols != width || orig_img.rows != height)
    {
        // 如果需要缩放,再对 resized_img 申请大小,节约内存开销
        resized_img.create(height, width, CV_8UC3);
        if (this->config.accels_2d == ACCELS_2D::ACC_OPENCV)
        {
            // 打包模型输入尺寸
            cv::Size target_size(width, height);
            float min_scale = std::min(scale_w, scale_h);
			scale_w = min_scale;
			scale_h = min_scale;
			letterbox(rgb_img, resized_img, pads, min_scale, target_size, this->config.opencl);
        }
        else if (this->config.accels_2d == ACCELS_2D::ACC_RGA)
        {
            ret = RGA_resize(rgb_img, resized_img);
            if (ret != 0) {
                cout << "resize_rga error" << endl;
            }
        }
        else {
            cout << "Unsupported 2D acceleration" << endl;
            return cv::Mat();
        }
        inputs[0].buf = resized_img.data;
    }
    else
    {
        inputs[0].buf = rgb_img.data;
    }

这部分与原作者保持一致,都是来自瑞芯微官方的代码。

四、对比分析

        以上大致梳理了方案 4 的整个调用过程,涉及一个硬件视频解码和三个 RGA 加速库的调用。因为单独使用硬件视频解码和 RGA 加速库都可以提高帧率,但是两个一起使用则反而降低了速度,所以问题应该是出在同时开启硬件视频解码和 RGA 加速库才会执行的代码,也就是 RGA_yuv420sp_to_bgr 这个函数,用于将 NV12 的格式转为 BGR888。

        我在 NV12_to_BGR 函数中,将 RGA_yuv420sp_to_bgr 替换为 OpenCV 软件转换,其余正常保持硬件加速,发现帧率变得不稳定,从 100 到 160 之间来回跳变,上限确实是突破了目前最高的 150 帧:

        不知道是不是 OpenCV 转换后的图像与 RGA 库转换的结果有数据对齐方面的问题,会导致图像数据错误,进而导致丢帧,因为我的 main 函数的逻辑有下面这条:

// 如果取出的图像为空,则跳过
if (img.empty())
    continue;

        我不确定是否有直接关系,但是如果执行过这条语句,从我计算 FPS 的原理来讲,确实会导致帧率下降,目前也在一直查找问题。

        目前可以确定的是,两种硬件加速的瓶颈就是 RGA 库的 RGA_yuv420sp_to_bgr 转换函数导致的,我准备继续研究 RGA 接口,优化代码。

        代码会公布至 Github,如果有小伙伴感兴趣,欢迎一起讨论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2326863.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ ---- 虚继承

一、什么是虚继承 虚继承就是子类中只有一份间接父类的数据。用于解决多继承中的父类为非虚继承时出现的二义性问题&#xff0c;即菱形继承问题。继承方式需要加上virtual关键字。 二、虚继承的特性 以菱形继承为例&#xff1a; 1.不使用虚继承 根据输出的大小和关系图&…

启幕数据结构算法雅航新章,穿梭C++梦幻领域的探索之旅——堆的应用之堆排、Top-K问题

人无完人&#xff0c;持之以恒&#xff0c;方能见真我&#xff01;&#xff01;&#xff01; 共同进步&#xff01;&#xff01; 文章目录 一、堆排引入之使用堆排序数组二、真正的堆排1.向上调整算法建堆2.向下调整算法建堆3.向上和向下调整算法建堆时间复杂度比较4.建堆后的排…

forms实现俄罗斯方块

说明&#xff1a; 我希望用forms实现俄罗斯方块 效果图&#xff1a; step1:C:\Users\wangrusheng\RiderProjects\WinFormsApp2\WinFormsApp2\Form1.cs using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms;namespace WinFor…

PHP回调后门

1.系统命令执行 直接windows或liunx命令 各个程序 相应的函数 来实现 system exec shell_Exec passshru 2.执行代码 eval assert php代码 系统 <?php eval($_POST) <?php assert($_POST) 简单的测试 回调后门函数call_user_func(1,2) 1是回调的函数 2是回调…

实操自动生成接口自动化测试用例

​这期抽出来的问题是关于如何使用Eolinker自动生成接口自动化测试用例&#xff0c;也就是将API文档变更同步到测试用例&#xff0c;下面是流程的示例解析。 导入并关联API文档和自动化测试用例 首先是登陆Eolinker&#xff0c;可以直接在线使用。 进入流程测试用例详情页&am…

Python数据类型-dict

Python数据类型-dict 字典是Python中一种非常强大且常用的数据类型&#xff0c;它使用键-值对(key-value)的形式存储数据。 1. 字典的基本特性 无序集合&#xff1a;字典中的元素没有顺序概念可变(mutable)&#xff1a;可以动态添加、修改和删除元素键必须唯一且不可变&…

0301-组件基础-react-仿低代码平台项目

文章目录 1 组件基础2 组件props3 React开发者工具结语 1 组件基础 React中一切都是组件&#xff0c;组件是React的基础。 组件就是一个UI片段拥有独立的逻辑和显示组件可大可小&#xff0c;可嵌套 组件的价值和意义&#xff1a; 组件嵌套来组织UI结构&#xff0c;和HTML一…

18-背景渐变与阴影(CSS3)

知识目标 理解背景渐变的概念和作用掌握背景渐变样式属性的语法与使用理解阴影效果的原理和应用场景掌握阴影样式属性的语法与使用 1. 背景渐变 1.1 线性渐变 运用CSS3中的“background-image:linear-gradient&#xff08;参数值&#xff09;;”样式可以实现线性渐变效果。 …

UE5学习记录part12

第15节&#xff1a; treasure 154 treasure: spawn pickups from breakables treasure是items的子类 基于c的treasure生成蓝图类 155 spawning actors: spawning treasure pickups 设置treasure的碰撞 蓝图实现 156 spawning actors from c &#xff1a; spawning our treas…

鸿蒙开发03样式相关介绍(一)

文章目录 前言一、样式语法1.1 样式属性1.2 枚举值 二、样式单位三、图片资源3.1 本地资源3.2 内置资源3.3 媒体资源3.4 在线资源3.5 字体图标3.6 媒体资源 前言 ArkTS以声明方式组合和扩展组件来描述应用程序的UI&#xff0c;同时还提供了基本的属性、事件和子组件配置方法&a…

一周掌握Flutter开发--9. 与原生交互(上)

文章目录 9. 与原生交互核心场景9.1 调用平台功能&#xff1a;MethodChannel9.1.1 Flutter 端实现9.1.2 Android 端实现9.1.3 iOS 端实现9.1.4 使用场景 9.2 使用社区插件9.2.1 常用插件9.2.2 插件的优势 总结 9. 与原生交互 Flutter 提供了强大的跨平台开发能力&#xff0c;但…

鸿蒙阔折叠Pura X外屏开发适配

首先看下鸿蒙中断点分类 内外屏开合规则 Pura X开合连续规则: 外屏切换到内屏,界面可以直接接续。内屏(锁屏或非锁屏状态)切换到外屏,默认都显示为锁屏的亮屏状态。用户解锁后:对于应用已适配外屏的情况下,应用界面可以接续到外屏。折叠外屏显示展开内屏显示折叠状态…

小程序中跨页面组件共享数据的实现方法与对比

小程序中跨页面/组件共享数据的实现方法与对比 在小程序开发中&#xff0c;实现不同页面或组件之间的数据共享是常见需求。以下是几种主要实现方式的详细总结与对比分析&#xff1a; 一、常用数据共享方法 全局变量&#xff08;getApp()&#xff09;、本地缓存&#xff08;w…

Java 大视界 -- 基于 Java 的大数据分布式计算在基因测序数据分析中的性能优化(161)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

DeepSeek-R1 模型现已在亚马逊云科技上提供

2025年3月10日更新—DeepSeek-R1现已作为完全托管的无服务器模型在Amazon Bedrock上提供。 2025年2月5日更新—DeepSeek-R1 Distill Llama 和 Qwen模型现已在Amazon Bedrock Marketplace和Amazon SageMaker JumpStart中提供。 在最近的Amazon re:Invent大会上&#xff0c;亚马…

Python数据可视化-第2章-使用matplotlib绘制简单图表

环境 开发工具 VSCode库的版本 numpy1.26.4 matplotlib3.10.1 ipympl0.9.7教材 本书为《Python数据可视化》一书的配套内容&#xff0c;本章为第2章 使用matplotlib绘制简单图表 本文主要介绍了折线图、柱形图或堆积柱形图、条形图或堆积条形图、堆积面积图、直方图、饼图或…

Redis 02

今天是2025/04/01 20:13 day 16 总路线请移步主页Java大纲相关文章 今天进行Redis 3,4,5 个模块的归纳 首先是Redis的相关内容概括的思维导图 3. 持久化机制&#xff08;深度解析&#xff09; 3.1 RDB&#xff08;快照&#xff09; 核心机制&#xff1a; 触发条件&#xff…

unity UI管理器

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events;// UI界面基类 public abstract class UIBase : MonoBehaviour {[Header("UI Settings")]public bool keepInStack true; // 是否保留在界面栈中public …

STRUCTBERT:将语言结构融入预训练以提升深度语言理解

【摘要】最近&#xff0c;预训练语言模型BERT&#xff08;及其经过稳健优化的版本RoBERTa&#xff09;在自然语言理解&#xff08;NLU&#xff09;领域引起了广泛关注&#xff0c;并在情感分类、自然语言推理、语义文本相似度和问答等各种NLU任务中达到了最先进的准确率。受到E…

16-CSS3新增选择器

知识目标 掌握属性选择器的使用掌握关系选择器的使用掌握结构化伪类选择器的使用掌握伪元素选择器的使用 如何减少文档内class属性和id属性的定义&#xff0c;使文档变得更加简洁&#xff1f; 可以通过属性选择器、关系选择器、结构化伪类选择器、伪元素选择器。 1. 属性选择…