Linux50:ROCKX+RV1126视频流检测人脸

一、Rockx+Rv1126视频流检测人脸的大体流程图

上图是rockx+rv1126的大体流程,首先要初始化模块包括VI模块、VENC模块、并启动VI模块采集视频流、rockx模块的初始化。初始化模块后,就要分两个线程处理了。

主线程是负责rockx对VI视频流的处理,并用OPENCV对人脸进行画框,最后把处理后的VI数据传输到VENC模块里面。

第二个线程rockx_face_detect_venc_thread,从VENC模块获取到H264的编码码流数据,并把VENC码流数据保存。

二.Rockx+Rv1126视频流检测人脸的代码截图

2.1. RV1126 模块初始化并启动 VI 工作

上图是RV1126模块的初始化,包括VI模块、VENC模块的初始化,初始化上述模块后,则调用RK_MPI_VI_StartStream启动VI开始采集摄像头的视频流。关于VI模块、VENC模块的初始化参数这里就不阐述了,因为之前的课程里面已经讲了很多次。

2.2. rockx 人脸检测模块的初始化

这段代码是初始化rockx的模块,首先要使用rockx_create_config 分配rockx_config_t结构体,并使用rockx_add_config 把对应的rockx路径配置进去,在我们的板子里面在**/userdata/rockx_data** 里面,并使用rockx_create创建rockx_handle_t句柄,

rockx_create的传参第一个参数rockx_handle_t结构体指针、

第二个参数rockx_module_t是 ROCKX_MODULE_FACE_DETECTION_V2, ROCKX_MODULE_FACE_DETECTION_V2是人脸检测的Version2模块、

第三个参数是rockx_config_t结构体指针、第四个参数默认是0。

2.3. 使用 rockx VI 模块的数据进行人脸检测处理

(图2.3.1)

(图2.3.2)

这部分代码是整个DEMO的核心,也是ROCKX检测VI视频数据的核心。图2.3.1是初始化rockx_image_t结构体,初始化需要传三个值分别是width = WIDTH(1920)、height = HEIGHT(1080)、pixel_format=ROCKX_PIXEL_FORMAT_YUV420SP_NV12。这三个值都需要和VI模块的配置是一样的。

初始化rockx_image_t 后,则需要通过RK_MPI_SYS_GetMediaBuffer获取每一帧VI模块的数据,并把每一帧VI模块的缓冲区和长度传输给rockx_image_t。具体的代码是rv1126_rockx_image.data = (uint8_t *)RK_MPI_MB_GetPtr(mb)(把每一帧VI缓冲区数据赋值到rockx_image_t的data)、rv1126_rockx_image.size = RK_MPI_MB_GetSize(mb)(把每一帧VI大小赋值到rockx_image_t的size)

赋值到rockx_image_t 后**,** 则调用rockx_face_detect 对每一帧的rockx_image_t图像进行人脸检测,并把人脸检测的结果输出到rockx_object_array_t rockx_object_array_t的内容主要存储的是人脸检测数量和人脸检测区域信息(如:left、top、right、bottom的坐标信息)

2.4. 使用 opencv 对人脸检测的结果进行画框

检测完每一帧人脸数据后就需要对每个人脸区域进行画框了,这里画框是用opencv进行处理。首先要先创建OPENCV的Mat矩阵,Mat rv1126_image_mat = Mat(HEIGHT, WIDTH, CV_8UC1, rv1126_rockx_image.data)

创建完Mat之后,则需要根据rockx_object_array_t 的坐标信息进行画框,先循环遍历人脸的数量(rockx_object_array_t.count),然后获取每一帧人脸的坐标信息,主要是left、top、right、bottom, 最后使用OPENCV的rectangle函数把坐标信息描绘出一个矩形表现出来。

2.5. 把处理后的数据发送到 VENC 模块

把上述的数据处理完成之后则把每一帧数据传输给VENC模块,这里使用的API是RK_MPI_SYS_SendMediaBuffer **。**此时此刻VENC模块就有VENC码流数据了

2.6. 创建 rockx_face_detect_venc _thread 线程保存每一帧 H264 的编码码流数据

(图2.6.1)

(图2.6.2)

通过pthread_create创建venc码流线程,这个线程的名字是rockx_face_detect_venc thread 如图(2.6.1) 在这个线程里面,通过RK_MPI_SYS_GetMediaBuffer 获取每一帧通过rockx人脸检测处理后的VENC码流数据,并用fwrite保存起来(fwrite(RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb), 1, face_detect_h264)),如图2.6.2。

最终输出的结果:

最终输出的结果是在视频中检测出对应的人脸并用opencv画矩形出来。

三、代码

cs 复制代码
/****************************************************************************
 *
 *    Copyright (c) 2017 - 2019 by Rockchip Corp.  All rights reserved.
 *
 *    The material in this file is confidential and contains trade secrets
 *    of Rockchip Corporation. This is proprietary information owned by
 *    Rockchip Corporation. No part of this work may be disclosed,
 *    reproduced, copied, transmitted, or used in any way for any purpose,
 *    without the express written permission of Rockchip Corporation.
 *
 *****************************************************************************/
#include <stdio.h>
#include <memory.h>
#include <sys/time.h>
#include "rknn_rockx_include/rockx_type.h"
#include "rknn_rockx_include/utils/rockx_config_util.h"
#include "rknn_rockx_include/utils/rockx_image_util.h"
#include "rockx.h"
#include <opencv2/opencv.hpp>
#include <opencv2/imgcodecs.hpp>

using namespace cv;

int main(int argc, char **argv)
{
    const char * img_path = argv[1];

    rockx_config_t * face_rockx_config = rockx_create_config();
    rockx_add_config(face_rockx_config,ROCKX_CONFIG_DATA_PATH,"/userdata/rockx_data/");

    rockx_handle_t face_rockx_handle;
    rockx_ret_t rockx_ret;
    rockx_module_t face_rock_module = ROCKX_MODULE_FACE_DETECTION_V2;
    rockx_ret = rockx_create(&face_rockx_handle,face_rock_module,face_rockx_config,0);
    if(rockx_ret != ROCKX_RET_SUCCESS)
    {
        printf("rockx_create failed \n");
        return -1;
    }
    printf("rockx_create success \n");

    rockx_image_t face_rockx_image;
    rockx_ret = rockx_image_read(img_path,&face_rockx_image,1);
    if(rockx_ret != ROCKX_RET_SUCCESS)
    {
        printf("rockx_image_read failed \n");
        return -1;
    }
    printf("rockx_image_read success \n");

    rockx_object_array_t face_rockx_object_array;
    rockx_ret = rockx_face_detect(face_rockx_handle,&face_rockx_image,&face_rockx_object_array,nullptr);
    if(rockx_ret != ROCKX_RET_SUCCESS)
    {
        printf("rockx_face_detect failed \n");
        return -1;
    }
    printf("rockx_face_detect success \n");

    Mat face_rockx_img = Mat(face_rockx_image.height,face_rockx_image.width,CV_8UC3,face_rockx_image.data);

    for(int i = 0;i < face_rockx_object_array.count;i++)
    {
        int left = face_rockx_object_array.object[i].box.left;
        int top = face_rockx_object_array.object[i].box.top;
        int w = face_rockx_object_array.object[i].box.right - face_rockx_object_array.object[i].box.left;
        int h = face_rockx_object_array.object[i].box.bottom - face_rockx_object_array.object[i].box.top;

        Rect boundingrect(left,top,w,h);
        rectangle(face_rockx_img,boundingrect,Scalar(255,255,0),1,8);
    }

    imwrite("output_rockx.jpg",face_rockx_img);
    rockx_destroy(face_rockx_handle);

    return 0;
}

四、代码详解

一、程序整体架构

二、头文件与宏定义

cs 复制代码
#include "rkmedia_api.h"      // RKMedia 多媒体API(VI/VENC等)
#include "rockx.h"             // RockX AI推理引擎API
#include <opencv2/opencv.hpp>  // OpenCV 图像处理

#define CAMERA_PATH "rkispp_scale0"  // ISP设备路径
#define CAMERA_ID 0                   // 摄像头ID
#define CAMERA_CHN 0                  // VI通道号
#define VENC_CHN 0                    // VENC通道号
#define WIDTH 1920                    // 图像宽度
#define HEIGHT 1080                   // 图像高度

三、全局变量与数据结构

cs 复制代码
// 用于在内存中共享图像数据的结构
// rockx_image_t: RockX AI引擎使用的图像格式
typedef struct {
    uint8_t* data;                    // 指向图像数据的指针
    int size;                         // 数据大小
    int width, height;                // 宽高
    rockx_pixel_format_t pixel_format; // 像素格式(NV12/RGB等)
} rockx_image_t;

// cv::Mat: OpenCV使用的图像格式(C++类)
// 包含 data指针、尺寸、通道数、引用计数等

四、编码线程函数

cs 复制代码
/**
 * @brief 编码线程:将视频帧编码为H264并写入文件
 * @param arg 线程参数(未使用)
 * @return void*
 * 
 * 工作流程:
 * 1. 打开H264文件
 * 2. 循环获取编码后的数据
 * 3. 写入文件
 */
void * get_vnec_pthread(void* arg)
{
    pthread_detach(pthread_self());  // 分离线程,自动回收资源

    FILE *file = fopen("fece_rockx.h264","w+");  // 创建输出文件
    MEDIA_BUFFER mb;  // 媒体缓冲区,存放编码后的H264数据

    while (1)
    {
        // 阻塞获取编码后的数据(-1表示无限等待)
        mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, VENC_CHN, -1);
        if(!mb)
        {
            printf("RK_MPI_SYS_GetMediaBuffer failed\n");
            continue;
        }

        // 将编码数据写入文件
        fwrite(RK_MPI_MB_GetPtr(mb),   // 数据指针
               RK_MPI_MB_GetSize(mb),  // 数据大小
               1, file);               // 写入文件
        
        RK_MPI_MB_ReleaseBuffer(mb);   // 释放缓冲区
    }
}

五、主函数详解

阶段1:VI(视频输入)初始化

cs 复制代码
/**
 * VI初始化:配置摄像头采集参数
 * 
 * 数据流:摄像头CMOS → ISP处理器 → DDR内存 → VI模块
 */
VI_CHN_ATTR_S vi_chn_attr;
vi_chn_attr.pcVideoNode = CAMERA_PATH;        // ISP设备路径 "/dev/rkispp_scale0"
vi_chn_attr.u32Width = 1920;                  // 采集宽度
vi_chn_attr.u32Height = 1080;                 // 采集高度
vi_chn_attr.enPixFmt = IMAGE_TYPE_NV12;       // 像素格式:YUV420 NV12
vi_chn_attr.enBufType = VI_CHN_BUF_TYPE_MMAP; // 内存映射类型(零拷贝)
vi_chn_attr.u32BufCnt = 3;                    // 缓冲区数量(3重缓冲)
vi_chn_attr.enWorkMode = VI_WORK_MODE_NORMAL; // 正常工作模式

// 设置VI通道属性
ret = RK_MPI_VI_SetChnAttr(CAMERA_ID, CAMERA_CHN, &vi_chn_attr);
// 使能VI通道(开始采集)
ret = RK_MPI_VI_EnableChn(CAMERA_ID, CAMERA_CHN);
// 启动数据流
ret = RK_MPI_VI_StartStream(CAMERA_ID, CAMERA_CHN);

NV12格式内存布局

总大小 = width × height × 1.5 = 1920×1080×1.5 = 3,110,400字节

┌────────────────────────────────────┐

│ Y平面 (亮度) - 2,073,600字节 │

│ 单通道,每个像素8位 │

├────────────────────────────────────┤

│ UV平面 (色度) - 1,036,800字节 │

│ 双通道交错 (U0,V0,U1,V1...) │

└────────────────────────────────────┘

阶段2:VENC(视频编码器)初始化

cs 复制代码
/**
 * VENC初始化:配置H264编码器
 * 
 * 作用:将原始NV12帧压缩为H264码流
 */
VENC_CHN_ATTR_S venc_chn_attr;
memset(&venc_chn_attr, 0, sizeof(VENC_CHN_ATTR_S));

// 编码属性配置
venc_chn_attr.stVencAttr.u32PicWidth = 1920;   // 编码宽度
venc_chn_attr.stVencAttr.u32PicHeight = 1080;  // 编码高度
venc_chn_attr.stVencAttr.imageType = IMAGE_TYPE_NV12;  // 输入格式
venc_chn_attr.stVencAttr.enType = RK_CODEC_TYPE_H264;  // 编码类型

// 码率控制:CBR (固定码率)
venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
venc_chn_attr.stRcAttr.stH264Cbr.u32Gop = 25;        // GOP大小(关键帧间隔)
venc_chn_attr.stRcAttr.stH264Cbr.u32BitRate = 1920 * 1080 * 3;  // 码率≈6.2Mbps
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25;  // 目标帧率25fps

// 创建编码通道
ret = RK_MPI_VENC_CreateChn(0, &venc_chn_attr);

阶段3:RockX(AI推理引擎)初始化

cs 复制代码
/**
 * RockX初始化:加载人脸检测模型到NPU
 * 
 * RockX是Rockchip的AI推理框架,利用NPU硬件加速
 */
rockx_config_t *face_rockx_config = rockx_create_config();
rockx_add_config(face_rockx_config, ROCKX_CONFIG_DATA_PATH, "/userdata/rockx_data/");

rockx_handle_t face_rockx_handle;
rockx_module_t face_rockx_module = ROCKX_MODULE_FACE_DETECTION_V2;  // 人脸检测V2模型

// 创建RockX句柄(加载模型到NPU)
rockx_ret = rockx_create(&face_rockx_handle, face_rockx_module, face_rockx_config, 0);

// 准备输入图像结构体
rockx_image_t face_img;
face_img.height = HEIGHT;      // 1080
face_img.width = WIDTH;        // 1920
face_img.pixel_format = ROCKX_PIXEL_FORMAT_YUV420SP_NV12;  // 匹配VI输出格式

阶段4:创建编码线程

cs 复制代码
pthread_t pid;
pthread_create(&pid, NULL, get_vnec_pthread, NULL);
// 独立线程负责将编码后的H264数据写入文件

阶段5:主循环 - 核心处理流程

cs 复制代码
while (1)
{
    // ═══════════════════════════════════════════════════════════
    // 步骤1:获取原始视频帧
    // ═══════════════════════════════════════════════════════════
    mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VI, CAMERA_CHN, -1);
    // 返回值:MEDIA_BUFFER结构体,包含:
    //   - ptr: 指向NV12数据的虚拟地址
    //   - size: 数据大小(3,110,400字节)
    //   - fd: DMA文件描述符
    
    // ═══════════════════════════════════════════════════════════
    // 步骤2:配置AI输入(零拷贝)
    // ═══════════════════════════════════════════════════════════
    face_img.data = (uint8_t *)RK_MPI_MB_GetPtr(mb);  // 直接指向同一块内存
    face_img.size = RK_MPI_MB_GetSize(mb);
    // 此时:face_img.data == mb->ptr (地址相同)
    
    // ═══════════════════════════════════════════════════════════
    // 步骤3:OpenCV Mat封装(零拷贝)
    // ═══════════════════════════════════════════════════════════
    Mat face_img_mat = Mat(HEIGHT, WIDTH, CV_8UC1, face_img.data);
    // 注意:CV_8UC1只处理Y平面(亮度),UV平面被忽略
    // 此时:face_img_mat.data == mb->ptr (同一个地址)
    
    // ═══════════════════════════════════════════════════════════
    // 步骤4:NPU人脸检测(推理阶段)
    // ═══════════════════════════════════════════════════════════
    rockx_object_array_t face_object_array;  // 存储检测结果
    // 执行AI推理(NPU硬件加速)
    rockx_ret = rockx_face_detect(face_rockx_handle, &face_img, &face_object_array, NULL);
    
    /* rockx_face_detect 内部流程:
     * 1. NPU读取 face_img.data 中的NV12数据
     * 2. 预处理:归一化、缩放、格式转换
     * 3. NPU推理:卷积神经网络计算
     * 4. 后处理:NMS(非极大值抑制)
     * 5. 输出:人脸框坐标(box)、关键点等
     */
    
    // ═══════════════════════════════════════════════════════════
    // 步骤5:绘制检测结果(画框阶段)
    // ═══════════════════════════════════════════════════════════
    for(int i = 0; i < face_object_array.count; i++)
    {
        // 获取人脸边界框坐标
        int left = face_object_array.object[i].box.left;
        int top = face_object_array.object[i].box.top;
        int w = face_object_array.object[i].box.right - left;
        int h = face_object_array.object[i].box.bottom - top;
        
        // 使用OpenCV绘制矩形
        Rect boundingRect(left, top, w, h);
        rectangle(face_img_mat, boundingRect, Scalar(255,255,0), 1);
        
        /* rectangle() 内部操作:
         * 直接向 face_img_mat.data 指向的内存写入白色像素
         * 由于 face_img_mat.data == mb->ptr
         * 所以原始NV12内存被直接修改
         */
    }
    
    // ═══════════════════════════════════════════════════════════
    // 步骤6:发送到编码器
    // ═══════════════════════════════════════════════════════════
    RK_MPI_SYS_SendMediaBuffer(RK_ID_VENC, VENC_CHN, mb);
    // VENC读取mb->ptr处的数据(已包含矩形框)
    // 编码为H264后,通过编码线程写入文件
    
    // ═══════════════════════════════════════════════════════════
    // 步骤7:释放缓冲区
    // ═══════════════════════════════════════════════════════════
    RK_MPI_MB_ReleaseBuffer(mb);  // 归还缓冲区给VI模块
}

六、内存共享与零拷贝机制

关键理解:三个数据结构指向同一块内存

零拷贝的优势

传统方式 本程序方式
采集→复制→AI→复制→编码 采集→AI→编码(直接操作原内存)
4次memcpy 0次memcpy
CPU负载高 CPU负载低
内存占用大 内存占用小

七、各阶段详细时序图

八、推理阶段详解

cs 复制代码
// 推理阶段 = NPU执行神经网络计算的过程

rockx_face_detect(handle, &face_img, &output, NULL);

/* 内部详细流程:
 * 
 * [预处理阶段]
 * 1. 检查输入格式(NV12)
 * 2. 数据归一化(0-255 → 0-1)
 * 3. 缩放到模型输入尺寸(如320×240)
 * 4. 内存对齐(NPU硬件要求)
 * 
 * [NPU推理阶段]
 * 5. DMA传输到NPU内部内存
 * 6. 逐层执行卷积、池化、激活函数
 * 7. 硬件并行计算(NPU有多个计算单元)
 * 
 * [后处理阶段]
 * 8. 解析输出张量
 * 9. 解码边界框坐标
 * 10. NMS去除重复检测
 * 11. 转换回原始图像坐标
 * 
 * [输出]
 * 12. 填充 rockx_object_array_t 结构
 */

// 检测结果结构
typedef struct {
    rockx_object_t object[ROCKX_MAX_OBJECT_NUM];
    int count;  // 检测到的人脸数量
} rockx_object_array_t;

typedef struct {
    rockx_rect_t box;      // 人脸框 {left, top, right, bottom}
    float score;           // 置信度 (0-1)
    int id;                // 类别ID(人脸=1)
} rockx_object_t;

九、潜在问题与改进建议

问题1:CV_8UC1 只处理Y平面

cs 复制代码
// 当前代码
Mat face_img_mat = Mat(HEIGHT, WIDTH, CV_8UC1, face_img.data);
// 问题:只有Y平面,UV平面被忽略

// 改进方案
Mat yuv_mat(HEIGHT * 3 / 2, WIDTH, CV_8UC1, face_img.data);
Mat bgr_mat;
cvtColor(yuv_mat, bgr_mat, COLOR_YUV2BGR_NV12);
rectangle(bgr_mat, rect, Scalar(255,0,0), 2);  // 彩色框

十、完整数据流总结

十一、关键概念总结

概念 解释
零拷贝 多个模块共享同一块物理内存,避免数据复制
NV12 YUV420半平面格式,1.5通道,摄像头常用格式
rockx_image_t RockX AI引擎专用格式,用于NPU推理输入
cv::Mat OpenCV图像容器,提供丰富的图像处理函数
rectangle() 直接修改内存中的像素值,实现画框
NPU推理 神经网络计算单元执行人脸检测模型
VI Video Input,视频输入模块
VENC Video Encoder,视频编码模块
相关推荐
平凡但不平庸的码农1 小时前
Go 语言常用标准库详解
开发语言·后端·golang
下载居1 小时前
Node.js(Javascript运行环境) 26.1
开发语言·javascript·node.js
范什么特西1 小时前
第一个Mybatis
java·开发语言·mybatis
超梦dasgg1 小时前
智慧充电系统计费定价服务Java 实现
java·开发语言·spring·微服务
Dragon Wu1 小时前
Taro v4.2.0 scss使用“@/xxx“的配置方法
前端·小程序·taro·scss
vx-程序开发2 小时前
PHP书店网站-计算机毕业设计源码05274
开发语言·php·课程设计
陈eaten2 小时前
windows上协调多版本python以及虚拟环境
开发语言·windows·python·pycharm·pip·虚拟环境·py
wordbaby2 小时前
如何封装一个生产级的 React Native 分页列表 Hook
前端·react native·react.js
一晌小贪欢2 小时前
告别 `datetime` 混乱:使用 Python 类型注解构建健壮的时间处理管道
开发语言·python·时间·时间类型·时间模块