
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 420与semi-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数据获取在从线程,那么这就涉及到多线程之间的处理
流程框图大致如下:

首先使用一个全局的缓冲区:
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");
}
}
执行流程分解:
- 当前线程:工作线程(处理完图像后)
- Lambda 捕获:
[imageCopy]值捕获 → 创建独立副本(深拷贝) - invokeMethod 调用:
- 检查
g_app(通常是QApplication)所属线程 → 主线程 - 因为是
QueuedConnection且跨线程 → 创建QMetaCallEvent事件 - 事件携带:Lambda 函数对象 + 捕获的
imageCopy副本
- 检查
- 事件投递:
postEvent(g_app, event)→ 放入主线程事件队列 - 主线程处理:
- 事件循环(
QApplication::exec())空闲时取出事件 - 执行 Lambda → 调用
updateLabelInMainThread(imageCopy) - 此时已在主线程 → 安全操作
QLabel/QPixmap
- 事件循环(
| 概念 | 本质 | 作用 | 代码中的角色 |
|---|---|---|---|
QMetaObject::invokeMethod | 元对象系统的运行时调用接口 | 实现跨线程/动态函数调用 | 将 UI 更新操作“投递”到主线程 |
Qt::QueuedConnection | 基于事件队列的异步连接 | 线程安全切换执行上下文 | 确保 Lambda 在主线程执行 |
| Lambda + 值捕获 | 匿名函数 + 数据副本 | 封装操作逻辑 + 避免数据竞争 | 携带深拷贝的 imageCopy 安全传递 |
核心思想:Qt 通过事件队列实现线程间通信,invokeMethod + QueuedConnection 是跨线程调用的“安全通道”,本质是将函数调用转化为事件,在目标线程的事件循环中执行,从而规避直接跨线程操作对象的风险。
经过上述步骤,我们就成功将VPSS获取的数据通过Qt多线程显示到屏幕上了