Qt多线程显示VPSS输出数据

Qt多线程显示VPSS输出数据

Qt多线程显示VPSS输出数据(基于海思mpp)

代码链接:https://gitee.com/kidwjb/codec_video_audio

在进行实际开发时有时候除了会使用视频编码(VENC模块)输出264文件推流以外,有时还需要进行实时图像显示在设备上,那么接下来笔者提供一种集成Qt基于海思mpp的实时图像显示方案

VPSS重要参数初始化示例:

// 通道1: 用于屏幕显示,设置为240x320
            vpss_cfg->chn_attr[chn].width = 240;
            vpss_cfg->chn_attr[chn].height = 320;
            vpss_cfg->chn_attr[chn].compress_mode = HI_COMPRESS_MODE_NONE;
            vpss_cfg->chn_attr[chn].pixel_format = HI_PIXEL_FORMAT_YVU_SEMIPLANAR_420;

            // 设置通道1的帧率控制(屏幕刷新率)
            vpss_cfg->chn_attr[chn].frame_rate.src_frame_rate = 30;
            vpss_cfg->chn_attr[chn].frame_rate.dst_frame_rate = 10;

通过VPSS获取多路视频流

一般在摄像头数据经过VI采集之后会经过VPSS进行处理,然后再去VENC进行编码,而我们则需要获取VPSS的输出数据以供我们进行图像显示。

获取VPSS输出数据有两种方式,一种时通过接口获取图像帧,另一种是直接将VPSS与VO绑定。这里我只讲第一种方式,因为该方式可搭配Qt使用灵活性更高

通过接口获取

ot_video_frame结构体

目前看手册729页,有一个接口ss_mpi_vpss_get_chn_frame用于用户从通道获取一帧处理完成的图像。

  • 其中第三个参数ot_video_frame_info的说明在手册274

这是一个视频图像帧信息结构体

typedef struct {
    ot_video_frame video_frame;
    td_u32     pool_id;
    ot_mod_id  mod_id;
} ot_video_frame_info;

其中有一个视频原始图像帧结构ot_video_frame:

typedef struct {
    td_u32              width;    //图像宽度
    td_u32              height;   //图像高度
    ot_video_field       field;    //帧场模式
    ot_pixel_format      pixel_format;//视频图像像素格式
    ot_video_format      video_format;//视频图像格式
    ot_compress_mode     compress_mode;//视频压缩模式
    ot_dynamic_range     dynamic_range;//动态范围
    ot_color_gamut       color_gamut; //色域范围
    td_u32              header_stride[OT_MAX_COLOR_COMPONENT];//图像压缩头跨距
    td_u32              stride[OT_MAX_COLOR_COMPONENT];//图像数据跨距
    td_phys_addr_t     header_phys_addr[OT_MAX_COLOR_COMPONENT];//压缩头物理地址
    td_phys_addr_t     phys_addr[OT_MAX_COLOR_COMPONENT];//图像数据物理地址
    td_void* ATTRIBUTE  header_virt_addr[OT_MAX_COLOR_COMPONENT];//压缩头虚拟地址,内核态虚拟地址
    td_void* ATTRIBUTE  virt_addr[OT_MAX_COLOR_COMPONENT];//图像数据虚拟地址,内核态虚拟地址
    td_u32              time_ref;//图像帧序列号
    td_u64              pts;//图像时间戳
    td_u64              user_data[OT_MAX_USER_DATA_NUM];//用户数据
    td_u32              frame_flag; /* frame_flag, can be OR operation. */
    ot_video_supplement  supplement;//图像的补充信息
} ot_video_frame;

可以根据这个结构体进行图像帧的操作

ss_mpi_vpss_get_chn_frame接口注意

td_s32 ss_mpi_vpss_get_chn_frame(ot_vpss_grp grp, ot_vpss_chn chn, ot_video_frame_info 
*frame_info, td_s32 milli_sec)

用户从通道获取一帧处理完成的图像

想要通过这个接口获取VPSS图像数据,需要注意几项:

1.只有在USER模式下,并且队列深度不为0,才能获取到图像

他们都在同一个结构体ot_vpss_chn_attr里:

typedef struct {
  td_bool             mirror_en;                /* RW; Range: [0, 1]; Mirror enable. */
  td_bool             flip_en;                  /* RW; Range: [0, 1]; Flip enable. */
  td_bool             border_en;                /* RW; Range: [0, 1]; Border enable. */
  /* RW; range: Hi3519DV500 = [64, 8192]; Width of target image. */
  td_u32              width;
  /* RW; range: Hi3519DV500 = [64, 8192]; Height of target image. */
  td_u32              height;
  td_u32              depth;                    /* RW; Range: [0, 8]; Depth of chn image list. */
  ot_vpss_chn_mode    chn_mode;                 /* RW; Work mode of vpss channel. */
  ot_video_format     video_format;             /* RW; Video format of target image. */
  ot_dynamic_range    dynamic_range;            /* RW; Dynamic range of target image. */
  ot_pixel_format     pixel_format;             /* RW; Pixel format of target image. */
  ot_compress_mode    compress_mode;            /* RW; Compression mode of the output. */
  ot_frame_rate_ctrl  frame_rate;               /* RW; Frame rate control info. */
  ot_border           border_attr;              /* RW; Border info. */
  ot_aspect_ratio     aspect_ratio;             /* RW; Aspect ratio info. */
} ot_vpss_chn_attr;
chn_attr->chn_mode                  = HI_VPSS_CHN_MODE_USER;
chn_attr->depth                     = 1;

将结构体内的成员修改成如上即可

2.Hi3516CV610开启通道卷绕时,无法获取到帧

笔者的芯片使用的是Hi3516CV610,所以需要关闭通道环绕模式

// Hi3516CV610: 必须禁用所有通道的wrap,否则无法get_chn_frame
  for (chn = 0; chn < HI_VPSS_MAX_PHYS_CHN_NUM; chn++) {
      vpss_cfg->wrap_attr[chn].enable = HI_FALSE;
      vpss_cfg->wrap_attr[chn].buf_line = 0;
      vpss_cfg->wrap_attr[chn].buf_size = 0;
  }

3.为通道分配VB内存池

  • 原则: 不同分辨率的通道需要不同大小的VB缓冲区
  • 计算: 使用 hi_common_get_pic_buf_cfg() 基于每个通道的实际分辨率计算VB大小
  • 分配: 每个通道需要独立的VB池(common_pool)
// Pool 0: 通道0 - 编码通道(原始分辨率)
buf_attr.width = enc_param->enc_size[0].width;
buf_attr.height = enc_param->enc_size[0].height;
buf_attr.compress_mode = HI_COMPRESS_MODE_NONE;
hi_common_get_pic_buf_cfg(&buf_attr, &calc_cfg);
vb_cfg->common_pool[0].blk_size = calc_cfg.vb_size;
vb_cfg->common_pool[0].blk_cnt  = 4;  // 通道0深度3 + 1余量

// Pool 1: 通道1 - 显示通道(240x320)
buf_attr.width = 240;
buf_attr.height = 320;
buf_attr.compress_mode = HI_COMPRESS_MODE_NONE;
hi_common_get_pic_buf_cfg(&buf_attr, &calc_cfg);
vb_cfg->common_pool[1].blk_size = calc_cfg.vb_size;
vb_cfg->common_pool[1].blk_cnt  = 8;  // 通道1深度6 + 2余量

通过上述操作即可通过ss_mpi_vpss_get_chn_frame接口获取得到VPSS的一帧图像

hi_vpss_grp grp = 0;
hi_vpss_chn chn = 1;  // 使用通道1(非压缩通道)
hi_video_frame_info frame_info = {};
ret = hi_mpi_vpss_get_chn_frame(grp, chn, &frame_info, 100);
if(ret == HI_SUCCESS)
{
printf("[VPSS] frame: %dx%d, format=%d, compress=%d, stride=[%d,%d]\r\n",
                       frame_info.video_frame.width, frame_info.video_frame.height,
                       frame_info.video_frame.pixel_format, frame_info.video_frame.compress_mode,
                       frame_info.video_frame.stride[0], frame_info.video_frame.stride[1]);
}

转换成RGB数据

现在我们已经获取到了一帧VPSS输出的yuv图像数据,需要转换成RGB数据从而在屏幕上显示

查看了官方手册,VGS不支持转换成RGB格式,下文为官方原文:

像素格式转换

VGS支持的输入输出像素格式包括semi-planar 420、semi-planar 422和单分量 (Y)。支持semi-planar 420semi-planar 422之间的格式转换,支持semi planar 420、semi-planar 422到单分量(Y)格式的转换,做像素格式转换时, 支持UV先后顺序可调整。

因此我们需要手动将YUV转换成RGB

首先确定我们获取得到的图像数据格式:OT_PIXEL_FORMAT_YVU_SEMIPLANAR_420

其本质就是YUV420SP

由上所述我们获取得到的ot_video_frame结构体成员包含图像数据的内核态虚拟地址,我们需要把它映射到用户态空间供我们使用,可以使用接口:

ss_mpi_sys_mmap_cached:

td_void *ss_mpi_sys_mmap_cached(td_phys_addr_t phys_addr, td_u32 size);
  • phys_addr :需映射的内存单元起始地址
  • size:映射的字节数。不能为0
  • 返回值:非0有效地址,0为无效地址

在获取到用户态的地址后就可以进行RGB转换,以下为AI写的算法,可能不准仅供参考

// YUV420SP (YVU Semi-Planar = NV21) → RGB888 转换(Limited Range BT.601)
static void ConvertYvuSemiPlanar420ToRgb(
    const uint8_t* y_plane,
    const uint8_t* vu_plane,
    uint8_t* rgb_data,
    int width,
    int height,
    int y_stride,
    int vu_stride
) {
    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
            uint8_t yy = y_plane[y * y_stride + x];
            int uv_row = y >> 1;
            int uv_col = x & ~1;

            uint8_t v = vu_plane[uv_row * vu_stride + uv_col + 0];
            uint8_t u = vu_plane[uv_row * vu_stride + uv_col + 1];

            // BT.601 Limited Range: Y∈[16,235], UV∈[16,240]
            int Y = yy - 16;
            int V = v - 128;
            int U = u - 128;

            int R = (298 * Y + 409 * V + 128) >> 8;
            int G = (298 * Y - 100 * U - 208 * V + 128) >> 8;
            int B = (298 * Y + 516 * U + 128) >> 8;

            int idx = (y * width + x) * 3;
            rgb_data[idx + 0] = static_cast<uint8_t>(CLIP(R));
            rgb_data[idx + 1] = static_cast<uint8_t>(CLIP(G));
            rgb_data[idx + 2] = static_cast<uint8_t>(CLIP(B));
        }
    }
}

Qt多线程显示

在获取得到RGB数据之后,我们就需要使用Qt显示到屏幕上

目前我们使用的是生产者消费者模式,Qt显示在主线程,而VPSS数据获取在从线程,那么这就涉及到多线程之间的处理

流程框图大致如下:

5d15db1448eb4dc8f7e4bc68e9e3d316fa692de6png

首先使用一个全局的缓冲区:

static QByteArray g_imageBuffer;  // 用于保存图像数据副本

然后把RGB数据拷贝到缓冲区

    // 使用互斥锁保护缓冲区更新
    QMutexLocker locker(&g_mutex);
    // 拷贝图像数据到缓冲区
    int dataSize = width * height * 3;
    g_imageBuffer.resize(dataSize);
    memcpy(g_imageBuffer.data(), rgb_data, dataSize);

接下来从缓冲区创建一个QImage

// 立即从缓冲区创建QImage
    QImage image(reinterpret_cast<const uchar*>(g_imageBuffer.constData()),
                 width, height, width * 3, QImage::Format_RGB888);

    if (image.isNull()) {
        qWarning("Failed to create QImage");
        printf("[qt_ui_update_image] ERROR: QImage is null\n");
        return -1;
    }

并且使用深拷贝方式,从而能够跨线程调用

// 创建深拷贝 (QImage 是可以跨线程传递的)
    QImage imageCopy = image.copy();

    locker.unlock();  // 提前解锁

最后通过异步队列处理,将事件投递到主线程中,触发屏幕刷新

// 使用 lambda 在主线程中创建 QPixmap 并更新 UI
    QMetaObject::invokeMethod(g_app, [imageCopy]() {
        updateLabelInMainThread(imageCopy);
    }, Qt::QueuedConnection);   


// 辅助函数:在主线程中从 QImage 更新 UI
static void updateLabelInMainThread(const QImage &image) {
    QPixmap pixmap = QPixmap::fromImage(image);
    if (!pixmap.isNull()) {
        g_label->setPixmap(pixmap);
        g_label->update();  // 强制刷新
        g_label->repaint(); // 立即重绘
        QCoreApplication::processEvents();  // 处理待处理事件
    } else {
        printf("[updateLabelInMainThread] ERROR: Failed to create QPixmap\n");
    }
}

执行流程分解:

  1. 当前线程:工作线程(处理完图像后)
  2. Lambda 捕获[imageCopy] 值捕获 → 创建独立副本(深拷贝)
  3. invokeMethod 调用
    • 检查 g_app(通常是 QApplication)所属线程 → 主线程
    • 因为是 QueuedConnection 且跨线程 → 创建 QMetaCallEvent 事件
    • 事件携带:Lambda 函数对象 + 捕获的 imageCopy 副本
  4. 事件投递postEvent(g_app, event) → 放入主线程事件队列
  5. 主线程处理
    • 事件循环(QApplication::exec())空闲时取出事件
    • 执行 Lambda → 调用 updateLabelInMainThread(imageCopy)
    • 此时已在主线程 → 安全操作 QLabel/QPixmap
概念本质作用代码中的角色
QMetaObject::invokeMethod元对象系统的运行时调用接口实现跨线程/动态函数调用将 UI 更新操作“投递”到主线程
Qt::QueuedConnection基于事件队列的异步连接线程安全切换执行上下文确保 Lambda 在主线程执行
Lambda + 值捕获匿名函数 + 数据副本封装操作逻辑 + 避免数据竞争携带深拷贝的 imageCopy 安全传递

核心思想:Qt 通过事件队列实现线程间通信,invokeMethod + QueuedConnection 是跨线程调用的“安全通道”,本质是将函数调用转化为事件,在目标线程的事件循环中执行,从而规避直接跨线程操作对象的风险。

经过上述步骤,我们就成功将VPSS获取的数据通过Qt多线程显示到屏幕上了

上一篇