【OpenGL】用OpenGL shader实现将YUV(YUV420,YV12)转RGB-(直接调用GPU实现,纯硬件方式,效率高) 这段时间一直在搞视频格式的转换问题,终于最近将一个图片的YUV

本文介绍如何使用OpenGL Shader将YUV420格式数据高效地转换为RGB格式,包括读取YUV数据、创建纹理及编写Shader代码等步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【OpenGL】用OpenGL shader实现将YUV(YUV420,YV12)转RGB-(直接调用GPU实现,纯硬件方式,效率高)

这段时间一直在搞视频格式的转换问题,终于最近将一个图片的YUV格式转RGB格式转换成功了。下面就来介绍一下:


由于我的工程是在vs2008中的,其中包含一些相关头文件和库,所以下面只是列出部分核心代码,并不是全部代码。


1、下载一个包含YUV数据的文件也可以自己制作一个该文件

下载地址: YUV数据文件

2、读入YUV数据文件中的yuv数据:

关键代码如下:

2.1读文件代码

unsigned char * readYUV(char *path)
{

	FILE *fp;
	unsigned char * buffer;
	long size = 1280 * 720 * 3 / 2;

	if((fp=fopen(path,"rb"))==NULL)
	{
	   printf("cant open the file");
	   exit(0);
	}

	buffer = new unsigned char[size];
	memset(buffer,'\0',size);
	fread(buffer,size,1,fp);
	fclose(fp);
	return buffer;
}

2.2读入数据,并将YUV数据分别制作成3个纹理

GLuint texYId;
GLuint texUId;
GLuint texVId;


void loadYUV(){
	int width ;
	int	height ;

	width = 640;
	height = 480;

	unsigned char *buffer = NULL;
	buffer = readYUV("1.yuv");

   glGenTextures ( 1, &texYId );
   glBindTexture ( GL_TEXTURE_2D, texYId );
   glTexImage2D ( GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, buffer );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );

   glGenTextures ( 1, &texUId );
   glBindTexture ( GL_TEXTURE_2D, texUId );
   glTexImage2D ( GL_TEXTURE_2D, 0, GL_LUMINANCE, width / 2, height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, buffer + width * height);
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );

   glGenTextures ( 1, &texVId );
   glBindTexture ( GL_TEXTURE_2D, texVId );
   glTexImage2D ( GL_TEXTURE_2D, 0, GL_LUMINANCE, width / 2, height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, buffer + width * height * 5 / 4 );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );

}

上述代码中1.yuv就是YUV数据文件

3、将纹理传入

上述片段shader中就是根据yuv转rgb的公式得来的。也就是说是在shader中实现转换的。
3.1 顶点shader和片段shader代码
GLbyte vShaderStr[] =  
     "attribute vec4 vPosition;        \n"
    "attribute vec2 a_texCoord;	 \n"
    "varying vec2 tc;		      \n"
     "void main()                      \n"
      "{                                \n"
      "   gl_Position = vPosition;      \n"
     "	  tc = a_texCoord;	      \n"
      "}                                \n";
   
   GLbyte fShaderStr[] =  
        "precision mediump float;                      \n"
	  "uniform sampler2D tex_y;					\n"
	  "uniform sampler2D tex_u;					\n"
	  "uniform sampler2D tex_v;					\n"
	  "varying vec2 tc;							\n"
      "void main()                                  \n"
      "{                                            \n"
	  "  vec4 c = vec4((texture2D(tex_y, tc).r - 16./255.) * 1.164);\n"
	  "  vec4 U = vec4(texture2D(tex_u, tc).r - 128./255.);\n"
	  "  vec4 V = vec4(texture2D(tex_v, tc).r - 128./255.);\n"
	  "  c += V * vec4(1.596, -0.813, 0, 0);\n"
	  "  c += U * vec4(0, -0.392, 2.017, 0);\n"
	  "  c.a = 1.0;\n"
	  "  gl_FragColor = c;\n"
      "}                                            \n";
上述片段shader中就是根据yuv转rgb的公式得来的。也就是说是在shader中实现转换的。

4、显示结果

结果如下:

注意:该shader是OpenGL格式的shader有一点差别。

--------------------------------------------------------------------------------------------------------------------------------

YV12格式与YUV格式只是在UV的存储位置上不同,需要注意一下

YV12,I420,YUV420P的区别

YV12和I420的区别
一般来说,直接采集到的视频数据是RGB24的格式,RGB24一帧的大小size=width×heigth×3 Byte,RGB32的size=width×heigth×4,如果是I420(即YUV标准格式4:2:0)的数据量是 size=width×heigth×1.5 Byte。
在采集到RGB24数据后,需要对这个格式的数据进行第一次压缩。即将图像的颜色空间由RGB2YUV。因为,X264在进行编码的时候需要标准的YUV(4:2:0)。但是这里需要注意的是,虽然YV12也是(4:2:0),但是YV12和I420的却是不同的,在存储空间上面有些区别。如下:
YV12 : 亮度(行×列) + V(行×列/4) + U(行×列/4)
I420 : 亮度(行×列) + U(行×列/4) + V(行×列/4)
可以看出,YV12和I420基本上是一样的,就是UV的顺序不同。
继续我们的话题,经过第一次数据压缩后RGB24->YUV(I420)。这样,数据量将减少一半,为什么呢?呵呵,这个就太基础了,我就不多写了。同样,如果是RGB24->YUV(YV12),也是减少一半。但是,虽然都是一半,如果是YV12的话效果就有很大损失。然后,经过X264编码后,数据量将大大减少。将编码后的数据打包,通过RTP实时传送。到达目的地后,将数据取出,进行解码。完成解码后,数据仍然是YUV格式的,所以,还需要一次转换,这样windows的驱动才可以处理,就是YUV2RGB24。

补充=============
详细的格式之间的差异可以参考:
 
查看YUV的时候也可以下载使用成熟的YUV播放器 ——YUV Player Deluxe: http://www.yuvplayer.com/
 

yuv420p就是I420格式,使用极其广泛,它的示意图:

 

【图像-视频处理】YUV420、YV12与RGB24的转换公式


[cpp]  view plain  copy print?
    1. bool YV12ToBGR24_Native(unsigned char* pYUV,unsigned char* pBGR24,int width,int height)  
    2. {  
    3.     if (width < 1 || height < 1 || pYUV == NULL || pBGR24 == NULL)  
    4.         return false;  
    5.     const long len = width * height;  
    6.     unsigned char* yData = pYUV;  
    7.     unsigned char* vData = &yData[len];  
    8.     unsigned char* uData = &vData[len >> 2];  
    9.   
    10.     int bgr[3];  
    11.     int yIdx,uIdx,vIdx,idx;  
    12.     for (int i = 0;i < height;i++){  
    13.         for (int j = 0;j < width;j++){  
    14.             yIdx = i * width + j;  
    15.             vIdx = (i/2) * (width/2) + (j/2);  
    16.             uIdx = vIdx;  
    17.   
    18.             bgr[0] = (int)(yData[yIdx] + 1.732446 * (uData[vIdx] - 128));                                    // b分量  
    19.             bgr[1] = (int)(yData[yIdx] - 0.698001 * (uData[uIdx] - 128) - 0.703125 * (vData[vIdx] - 128));    // g分量  
    20.             bgr[2] = (int)(yData[yIdx] + 1.370705 * (vData[uIdx] - 128));                                    // r分量  
    21.   
    22.             for (int k = 0;k < 3;k++){  
    23.                 idx = (i * width + j) * 3 + k;  
    24.                 if(bgr[k] >= 0 && bgr[k] <= 255)  
    25.                     pBGR24[idx] = bgr[k];  
    26.                 else  
    27.                     pBGR24[idx] = (bgr[k] < 0)?0:255;  
    28.             }  
    29.         }  
    30.     }  
    31.     return true;  

以上是yv12到RGB24的转换算法,如果是yuv420到RGB24转换,秩序u,v反过来就可以了。

即:

[cpp]  view plain  copy print?
  1. unsigned char* uData = &yData[nYLen];  
  2. unsigned char* vData = &vData[nYLen>>2];  
 注:海康威视网络摄像头一般就是yu12格式的!

2016-9-22 19:53

张朋艺 pyZhangBIT2010@126.com

 

找到的英文参考资料:

yv12 to rgb using glsl in iOS ,result image attached

https://stackoverflow.com/questions/11093061/yv12-to-rgb-using-glsl-in-ios-result-image-attached

following is my code for uploading the three planar data to textures:
- (GLuint) textureY: (Byte*)imageData        
      widthType: (int) width       
     heightType: (int) height       
{          
    GLuint texName;    
    glGenTextures( 1, &texName );     
    glBindTexture(GL_TEXTURE_2D, texName);

    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);    

    glTexImage2D( GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, imageData );  
    //free(imageData);

    return texName;    
}    

- (GLuint) textureU: (Byte*)imageData        
          widthType: (int) width       
         heightType: (int) height       
{          
    GLuint texName;    

    glGenTextures( 1, &texName );     
    glBindTexture(GL_TEXTURE_2D, texName);


    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);    

    glTexImage2D( GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, imageData );    

    //free(imageData);
    return texName;    
}    

- (GLuint) textureV: (Byte*)imageData        
          widthType: (int) width       
         heightType: (int) height       
{          
    GLuint texName;    
    glGenTextures( 1, &texName );     
    glBindTexture(GL_TEXTURE_2D, texName);


    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);    
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);    

    glTexImage2D( GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, imageData );    

    //free(imageData);
    return texName;    
}    


- (void) readYUVFile     
{    
    NSString *file = [[NSBundle mainBundle] pathForResource:@"video" ofType:@"yv12"];
    NSLog(@"%@",file);
    NSData* fileData = [NSData dataWithContentsOfFile:file]; 
    //NSLog(@"%@",[fileData description]);
    NSInteger width  = 352;    
    NSInteger height = 288;
    NSInteger uv_width  = width  / 2;    
    NSInteger uv_height = height / 2;
    NSInteger dataSize = [fileData length];
    NSLog(@"%i\n",dataSize);

    GLint nYsize  = width * height;     
    GLint nUVsize = uv_width * uv_height;      
    GLint nCbOffSet = nYsize;    
    GLint nCrOffSet = nCbOffSet + nUVsize;    

    Byte *spriteData = (Byte *)malloc(dataSize);
    [fileData getBytes:spriteData length:dataSize];


    Byte* uData = spriteData + nCbOffSet;
    //NSLog(@"%@\n",[[NSData dataWithBytes:uData length:nUVsize] description]);
    Byte* vData = spriteData + nCrOffSet;  
    //NSLog(@"%@\n",[[NSData dataWithBytes:vData length:nUVsize] description]);
    /**
    Byte *YPlanarData = (Byte *)malloc(nYsize);
    for (int i=0; i<nYsize; i++) {
        YPlanarData[i]= spriteData[i];
    }    

    Byte *UPlanarData = (Byte *)malloc(nYsize);
    for (int i=0; i<height; i++) {
        for (int j=0; j<width; j++) {
            int numInUVsize = (i/2)*uv_width+j/2;
            UPlanarData[i*width+j]=uData[numInUVsize];
        }
    }

    Byte *VPlanarData = (Byte *)malloc(nYsize);
    for (int i=0; i<height; i++) {
        for (int j=0; j<width; j++) {
            int numInUVsize = (i/2)*uv_width+j/2;
            VPlanarData[i*width+j]=vData[numInUVsize];
        }
    }
    **/

_textureUniformY = glGetUniformLocation(programHandle, "SamplerY"); 
_textureUniformU = glGetUniformLocation(programHandle, "SamplerU"); 
_textureUniformV = glGetUniformLocation(programHandle, "SamplerV");     

    free(spriteData);

}

and my fragment shaders code:

   precision highp float;
uniform sampler2D SamplerY;
uniform sampler2D SamplerU;
uniform sampler2D SamplerV;

varying highp vec2 coordinate;

void main()
{
    highp vec3 yuv,yuv1;
    highp vec3 rgb;

    yuv.x = texture2D(SamplerY, coordinate).r;

    yuv.y = texture2D(SamplerU, coordinate).r-0.5;

    yuv.z = texture2D(SamplerV, coordinate).r-0.5 ;

   rgb = mat3(      1,       1,      1,
                     0, -.34414, 1.772,
               1.402, -.71414,      0) * yuv;

    gl_FragColor = vec4(rgb, 1);
}
 
<think>我们结合用户的问题:使用Qt和C++结合海康威视SDK实现多相机视频获取并在Windows下显示。根据引用内容,我们知道海康SDK提供了设备登录、视频流获取等功能,而Qt可以用于界面显示和事件处理。实现步骤大致如下:1.初始化海康SDK。2.登录设备(每个相机需要单独登录)。3.为每个相机开启预览(实时视频流)。4.通过SDK的回调函数获取视频数据。5.将获取到的视频数据转换为Qt可识别的格式(例如QImage)。6.使用Qt的绘图机制(例如QLabel或自定义QWidget)显示视频。注意:多相机的情况,需要同时管理多个设备登录和多个实时流,并注意资源管理和线程安全。具体步骤:第一步:初始化SDK在程序启动时初始化海康SDK,退出时清理。第二步:登录设备每个相机用其IP地址、端口、用户名和密码登录,获取登录ID(每个设备一个登录ID)。第三步:实时预览为每个设备开启预览,设置回调函数。在回调函数中,SDK会将视频数据(YV12RGB等格式)传送给我们。第四步:回调函数处理在回调函数中,我们将视频帧数据转换为Qt支持的图像格式(例如RGB32),然后发送信号到Qt主线程进行显示。第五步:显示在Qt中,我们可以使用QLabel或自定义QWidget来显示图像。在多相机情况下,可能需要多个显示窗口。第六步:退出时释放资源停止预览,注销登录,释放SDK资源。由于视频数据回调是在SDK的线程中调用的,所以不能直接在回调中更新界面。我们需要将图像数据通过信号槽传递到主线程进行显示。代码结构示例:1.初始化SDK:```c++#include"HCNetSDK.h"...NET_DVR_Init();```2.定义设备登录结构体并登录:```c++NET_DVR_USER_LOGIN_INFOloginInfo={0};NET_DVR_DEVICEINFO_V40deviceInfo={0};loginInfo.bUseAsynLogin=false;strcpy(loginInfo.sDeviceAddress,"192.168.1.64");//设备IPloginInfo.wPort=8000;//设备服务端口strcpy(loginInfo.sUserName,"admin");strcpy(loginInfo.sPassword,"12345");LONGlUserID=NET_DVR_Login_V40(&loginInfo,&deviceInfo);```3.开启预览并设置回调函数:```c++//预览参数NET_DVR_PREVIEWINFOpreviewInfo={0};previewInfo.hPlayWnd=NULL;//窗口句柄,我们可以设置为NULL,然后在回调中自己绘制previewInfo.lChannel=1;//通道号previewInfo.dwStreamType=0;//主码流previewInfo.dwLinkMode=0;//TCP方式previewInfo.bBlocked=true;//阻塞取流//设置回调函数LONGlRealPlayHandle=NET_DVR_RealPlay_V40(lUserID,&previewInfo,RealDataCallBack,NULL);```4.回调函数的实现(非主线程):```c++voidCALLBACKRealDataCallBack(LONGlRealHandle,DWORDdwDataType,BYTE*pBuffer,DWORDdwBufSize,void*pUser){if(dwDataType==NET_DVR_STREAMDATA){//处理视频流数据//假设是RGB数据(具体要看设置的码流格式),或者YV12等,需要转换//将数据转换为QImage,注意:这里可能需要根据实际格式转换,海康默认可能是YV12//转换工作可以在这里做,也可以发送到另一个线程转换。但注意不要阻塞回调。//将图像数据通过信号发送到主线程(注意:这里需要跨线程传递,可以使用Qt的信号槽,但回调函数不在QObject派生类中,所以需要其他方式)//我们可以在这里将数据复制到缓冲区,然后通知显示线程处理。}}```5.由于回调函数是C风格的,我们需要将数据传递到Qt对象中。一种方式是使用全局变量或单例,但更合理的方式是在回调函数中将数据发送到一个Qt对象(通过信号槽,但回调函数不是Qt对象,因此不能直接发送信号)。我们可以:-在回调函数中,获取到数据后,通过Qt的元对象系统,将事件发送到主窗口对象。例如:```c++//在回调函数中:if(dwDataType==NET_DVR_STREAMDATA){//复制数据(注意避免频繁的内存分配)//然后使用QMetaObject::invokeMethod将数据发送到主窗口进行处理}```-或者,我们可以在回调函数中,将数据放入一个队列,然后由定时器或另一个线程处理并刷新UI。6.考虑到效率,我们可以在回调函数中将视频帧数据放入一个线程安全的队列,然后由一个专门的显示线程(或主线程定时检查)取数据并转换和显示。7.显示部分:在Qt中,我们可以自定义一个QWidget,重写paintEvent,在该事件中绘制图像。然后,我们在主界面中为每个相机创建一个这样的显示控件。8.多相机的情况:需要分别登录每个相机,并为每个相机设置独立的回调和显示控件。9.注意资源释放:当关闭时,按顺序停止预览,注销登录,清理SDK。由于视频处理涉及格式转换(例如YV12RGB),这里提供一个简单的转换方法(也可以使用OpenCV或Qt的QImage进行转换,但效率可能不高)。海康SDK也提供了转换函数:NET_DVR_ConvertToBMP_FILE等,但如果是实时显示,我们需要快速的转换。另一种高效的做法是使用GPU进行绘制(如引用[2]所述)。但用户问题没有特别要求GPU,所以我们可以先使用CPU转换。这里我们提供一个使用CPU转换YV12RGB32的简单方法(海康默认是YV12格式):我们可以使用libyuv库,或者手写转换算法。但考虑到实时性,我们也可以直接使用QImage支持的方式来显示(但QImage不支持直接显示YV12)。因此,我们可以用以下方式-YV12数据转换RGB32,然后构造QImage。转换函数可以自己写,也可以使用第三方库。手写转换代码(效率较低,仅作示例):```c++//YV12格式:Y平面,然后是V平面(高度一半),然后是U平面(高度一半)//注意:YV12是每4个Y共用一组U和V,即4:2:0//转换函数voidconvertYV12toRGB(constunsignedchar*yv12,unsignedchar*rgb,intwidth,intheight){//实现YV12RGB的算法,这里省略,实际中推荐使用快速算法或SSE优化//或者使用QImage::Format_RGB32配合转换后的数据}```不过,Qt从5.4开始支持QVideoFrame,但这里我们使用最直接的方法:转换后构造QImage。考虑到性能,我们可以使用OpenCV进行转换,或者使用GPU(如引用[2]所述)。但用户问题中未要求,我们采用CPU转换。另外,我们可以设置海康SDK输出RGB格式,这样就不需要转换了。海康SDK支持设置回调的视频格式。在开启预览时,我们可以设置预览参数为RGB32格式。但是,在预览参数中并没有直接设置输出格式的选项,而是通过设置回调的类型来决定。在设置预览之前,我们可以设置回调的数据格式:```c++NET_DVR_SetRealDataCallBack_V40(lRealHandle,RealDataCallBack,0,0,0);```但实际上,根据海康SDK文档,我们需要在预览参数中指定流类型,并且默认的数据格式是YV12。所以,我们可能需要自己转换。因此,步骤为:1.在回调函数中,如果数据是YV12格式,转换RGB32。2.将转换后的RGB32数据(连续内存,大小为width*height*4)构造为QImage,注意QImage默认是RGB32(即0xffRRGGBB,但实际上是BGRA的布局?),我们需要确保格式匹配。构造QImage:```c++QImageimg(rgb_data,width,height,width*4,QImage::Format_RGB32);//注意:这里的rgb_data是连续内存,每行有width*4字节(每像素4字节)//然后我们可以将其缩放(如果需要)并显示在QLabel或自定义控件上```但是,转换YV12RGB32是CPU密集型操作,多路视频可能占用大量CPU。因此,多路的情况下,建议采用GPU加速(如引用[2]所述),但本问题不展开。10.显示控件:我们创建一个继承自QWidget的类,用于显示视频帧。在这个类中,我们有一个setImage函数来更新图像,并在paintEvent中绘制。```c++classVideoWidget:publicQWidget{Q_OBJECTpublic:explicitVideoWidget(QWidget*parent=nullptr);voidsetImage(constQImage&img);protected:voidpaintEvent(QPaintEvent*event)override;private:QImagecurrentImage;};voidVideoWidget::setImage(constQImage&img){currentImage=img.copy();//避免数据被修改update();//触发重绘}voidVideoWidget::paintEvent(QPaintEvent*event){QPainterpainter(this);if(!currentImage.isNull()){painter.drawImage(rect(),currentImage.scaled(size(),Qt::KeepAspectRatio));}}```11.在回调处理中,转换图像后,通过信号槽将图像传递到显示控件。考虑到多相机,我们可以将每个相机的视频帧传递到对应的显示控件。12.多线程处理:由于回调线程不是Qt主线程,我们使用信号槽机制传递图像(确保安全)。我们设计一个中间对象,用于发图像(在回调线程中发送信号到显示控件所在线程)。这个中间对象(如FrameHandler)应该继承自QObject,并定义信号。然后在回调函数中,通过该对象发送信号。回调函数中如何访问到FrameHandler?我们可以将FrameHandler对象指针作为回调的userData传入。但是回调函数是C函数,所以需要static对象或全局的。一种做法是:在启动预览时将每个相机对应的FrameHandler(或显示控件)的指针传入userData(但注意多线程访问控件的问题)。更好的方式是将相机通道与显示控件建立映射关系。为了简化,我们可以在主窗口类(QObject)中创建一个槽函数,用于接收图像数据(包括图像和相机通道信息)。然后在回调函数中,通过元对象系统调用主窗口的槽函数。回调函数示例(使用全局函数,然后发到主窗口):```c++voidCALLBACKRealDataCallBack(LONGlRealHandle,DWORDdwDataType,BYTE*pBuffer,DWORDdwBufSize,void*pUser){//pUser是我们设置预览时传入的用户数据,这里我们传入主窗口对象指针(即QObject*)if(dwDataType==NET_DVR_STREAMDATA){//获取主窗口对象QObject*mainWindow=static_cast<QObject*>(pUser);//这里我们得到视频帧数据,需要转换格式,但转换耗时,因此我们需要复制数据,然后在主线程转换?//为了不阻塞回调线程,我们先将原始数据复制一份,然后发送到主线程处理//注意:pBuffer中的数据大小和格式:YV12,需要知道宽高//宽高如何获取?可以在主窗口对象中记录每个lRealHandle对应的宽高?或者从设备信息获取?但这里没有宽高。//我们可以通过NET_DVR_GetDVRConfig来获取通道配置,但比较复杂。另一种方法是在回调的第一个关键帧后解析数据(通常第一帧是I帧,包含SPS/PPS,可以解析出宽高),但我们这里简化,假设宽高已知(启动预览前已配置的分辨率)。//因此,我们需要在开启预览时记录每个预览句柄对应的宽高(或者通道的宽高),然后传递到回调处理。//复制数据QByteArraybuffer((constchar*)pBuffer,dwBufSize);QMetaObject::invokeMethod(mainWindow,"OnVideoData",Qt::QueuedConnection,Q_ARG(LONG,lRealHandle),Q_ARG(QByteArray,buffer));}}```然后在主窗口中:```c++voidMainWindow::OnVideoData(LONGlRealHandle,QByteArraydata){//根据lRealHandle查找对应的显示控件(或FrameHandler)以及该通道的宽高//假设我们有一个map:m_realHandleMap<LONG,VideoWidget*>,建立预览句柄和显示控件的映射VideoWidget*widget=m_realHandleMap.value(lRealHandle);if(!widget)return;//假设宽高已知(在启动预览时保存到另一个map中)QSizesize=m_realHandleSize.value(lRealHandle);//将YV12数据转换RGB32QImageimg=convertYV12ToRGB(data,size.width(),size.height());widget->setImage(img);}```但是,转换操作如果放在主线程进行,多路视频可能会导致主线程阻塞,界面卡顿。因此,建议使用多线程转换:创建几个工作线程(线程池)来转换图像,转换完成后再发送到对应的显示控件。考虑到复杂度,如果我们只有少量相机(如2~4个),且分辨率不是特别高(如720P),可以在主线程进行转换。否则,必须使用工作线程。由于问题只要求实现,我们以最简单的方式实现转换(在主线程转换),并注意优化转换效率。总结步骤:-初始化海康SDK。-设计主窗口,包含多个VideoWidget(例如在网格布局中)。-登录多个相机设备,记录登录ID。-为每个相机开启预览,并传入主窗口指针(作为userData)以便回调使用。-在开启预览前,需要知道通道的分辨率(可以通过配置获取或者固定值)。这里我们假设每个相机都是固定的分辨率(比如640x480),在代码中写死。实际项目应该从设备配置读取。-将每个预览句柄(lRealPlayHandle)和对应的显示控件以及宽高信息关联起来(用map)。-在回调函数中,将数据复制并通过invokeMethod传递到主窗口的一个槽函数。-在主窗口的槽函数中,将YV12数据转换RGB32,然后设置给对应的VideoWidget显示。由于海康SDK回调数据可能包含关键帧和非关键帧,以及编码帧,但我们这里使用实时流数据(NET_DVR_STREAMDATA)作为视频流,所以是已经解码的原始数据(YV12)。13.退出程序:停止所有预览,注销登录,释放资源。注意:海康SDK在回调函数中不要做耗时操作。最后,提供一个简化的代码结构示例(不包含错误处理):初始化:```c++//在main函数中NET_DVR_Init();```主窗口头文件(mainwindow.h):```c++#include<QMainWindow>#include"videowidget.h"#include<map>#include<HCNetSDK.h>namespaceUi{classMainWindow;}classMainWindow:publicQMainWindow{Q_OBJECTpublic:explicitMainWindow(QWidget*parent=nullptr);~MainWindow();publicslots:voidOnVideoData(LONGlRealHandle,QByteArraydata);private:Ui::MainWindow*ui;//存储设备登录IDQMap<QString,LONG>m_deviceLoginIDMap;//用设备标识(如IP)作为key//存储预览句柄和对应的显示控件QMap<LONG,VideoWidget*>m_realHandleToWidgetMap;//存储预览句柄对应的宽高QMap<LONG,QSize>m_realHandleToSizeMap;};```主窗口源文件(mainwindow.cpp)部分代码:```c++MainWindow::MainWindow(QWidget*parent):QMainWindow(parent),ui(newUi::MainWindow){ui->setupUi(this);//初始化界面,比如创建多个VideoWidget(根据相机数量)//假设我们需要4个相机for(inti=0;i<4;++i){VideoWidget*widget=newVideoWidget(this);//添加到布局(这里省略布局代码)//假设布局是gridLayout,两行两列ui->gridLayout->addWidget(widget,i/2,i%2);//暂时不关联预览句柄,在登录后关联}//登录相机,开启预览QStringListcameraIPs={"192.168.1.64","192.168.1.65","192.168.1.66","192.168.1.67"};for(inti=0;i<cameraIPs.size();++i){NET_DVR_USER_LOGIN_INFOloginInfo={0};NET_DVR_DEVICEINFO_V40deviceInfo={0};loginInfo.bUseAsynLogin=false;strncpy(loginInfo.sDeviceAddress,cameraIPs[i].toLocal8Bit().constData(),sizeof(loginInfo.sDeviceAddress));loginInfo.wPort=8000;strncpy(loginInfo.sUserName,"admin",sizeof(loginInfo.sUserName));strncpy(loginInfo.sPassword,"12345",sizeof(loginInfo.sPassword));LONGlUserID=NET_DVR_Login_V40(&loginInfo,&deviceInfo);if(lUserID<0){//登录失败continue;}m_deviceLoginIDMap[cameraIPs[i]]=lUserID;//开启预览NET_DVR_PREVIEWINFOpreviewInfo={0};previewInfo.hPlayWnd=NULL;previewInfo.lChannel=1;//通道号,假设都是通道1previewInfo.dwStreamType=0;//主码流previewInfo.dwLinkMode=0;//TCPpreviewInfo.bBlocked=true;//设置预览句柄对应的宽高(这里假设相机分辨率为640x480)LONGlRealHandle=NET_DVR_RealPlay_V40(lUserID,&previewInfo,RealDataCallBack,this);//注意最后一个参数传入this作为userDataif(lRealHandle<0){//开启预览失败continue;}//将预览句柄与显示的VideoWidget关联m_realHandleToWidgetMap[lRealHandle]=dynamic_cast<VideoWidget*>(ui->gridLayout->itemAtPosition(i/2,i%2)->widget());m_realHandleToSizeMap[lRealHandle]=QSize(640,480);}}voidMainWindow::OnVideoData(LONGlRealHandle,QByteArraydata){VideoWidget*widget=m_realHandleToWidgetMap.value(lRealHandle);if(!widget)return;QSizesize=m_realHandleToSizeMap.value(lRealHandle);if(size.isEmpty())return;//转换YV12RGB32QImageimg=convertYV12ToRGB(data.constData(),size.width(),size.height());if(!img.isNull()){widget->setImage(img);}}//转换函数(示例,未实现)QImageMainWindow::convertYV12ToRGB(constchar*yv12Data,intwidth,intheight){//实现转换//这里使用QImage::Format_RGB32QImageimg(width,height,QImage::Format_RGB32);//转换代码省略...returnimg;}MainWindow::~MainWindow(){//停止预览和注销for(autoit=m_deviceLoginIDMap.begin();it!=m_deviceLoginMap.end();++it){//需要找到该设备的所有预览句柄,这里我们简化处理:登录ID对应的每个预览句柄(实际每个设备可能有多个预览)//但我们的实现一个设备只开了一个预览autorealHandles=m_realHandleToWidgetMap.keys();for(autorealHandle:realHandles){NET_DVR_StopRealPlay(realHandle);}NET_DVR_Logout(it.value());}NET_DVR_Cleanup();deleteui;}//全局的回调函数voidCALLBACKRealDataCallBack(LONGlRealHandle,DWORDdwDataType,BYTE*pBuffer,DWORDdwBufSize,void*pUser){if(dwDataType==NET_DVR_STREAMDATA){MainWindow*mainWindow=static_cast<MainWindow*>(pUser);//复制数据QByteArraybuffer((constchar*)pBuffer,dwBufSize);//传递到主窗口QMetaObject::invokeMethod(mainWindow,"OnVideoData",Qt::QueuedConnection,Q_ARG(LONG,lRealHandle),Q_ARG(QByteArray,buffer));}}```由于时间关系,转换函数convertYV12ToRGB没有实现。实际应用中可以使用开源库(如libyuv)进行转换,或者使用海康SDK的转换函数(例如NET_DVR_ConvertToBMP_FILE,但这个只能保存文件,不适合实时显示)。我们提供一种简单的转换代码(但效率低,建议优化):```c++QImageconvertYV12ToRGB(constchar*yv12Data,intwidth,intheight){constintframeSize=width*height;constintySize=frameSize;constintuvSize=frameSize/4;//因为4:2:0//分配目标图像QImagergbImg(width,height,QImage::Format_RGB32);constuchar*yData=(constuchar*)yv12Data;constuchar*vData=yData+ySize;//Y之后是V平面constuchar*uData=vData+uvSize;//V之后是U平面for(inty=0;y< height;y++){uchar*rgb=rgbImg.scanLine(y);intuvRow=y/2;//YV12的UV平面行是隔行取for(intx=0;x< width;x++){intyVal=yData[y*width+x];intuvCol=x/2;intuVal=uData[uvRow*(width/2)+uvCol];intvVal=vData[uvRow*(width/2)+uvCol];//YUVRGB公式(注意:标准不同公式有差异,这里是ITU-RBT.601)intc=yVal-16;intd=uVal-128;inte=vVal-128;intr=(298*c+409*e+128)>>8;intg=(298*c-100*d-208*e+128)>>8;intb=(298*c+516*d+128)>>8;r=qBound(0,r,255);g=qBound(0,g,255);b=qBound(0,b,255);*rgb++=b;*rgb++=g;*rgb++=r;*rgb++=0;//第四字节(Alpha)设为0(不透明)}}returnrgbImg;}```上述转换是每个像素单独计算,效率不高。可以考虑使用查表法或并行计算优化。由于用户可能对性能有要求,多路视频时,建议使用GPU进行转换和绘制(如引用[2]),或者使用OpenGL显示YUV纹理(这样就不需要转换RGB,而是直接上传YUV数据到GPU,然后用shader转换)。但这超出了本问题的范围。注意:上述代码只是一个框架,实际应用中需要处理错误、断线重连等情况。以上,我们给出了使用Qt和C++结合海康威视SDK实现多相机视频获取并在Windows下显示的方案。</think> 根据您的需求,结合海康威视SDK和Qt框架实现多相机视频获取与显示,以下是完整的实现方案: ### 一、实现流程 1. **环境准备** - 下载海康威视SDK(Windows版) - 安装Qt 5.x+(MSVC编译器) - 配置工程包含路径: ```cpp INCLUDEPATH += $$PWD/HCNetSDK/include LIBS += -L$$PWD/HCNetSDK/lib -lHCNetSDK -lPlayCtrl ``` 2. **核心实现步骤** ```mermaid graph TD A[初始化SDK] --> B[设备登录] B --> C[启动预览流] C --> D[注册回调函数] D --> E[视频数据处理] E --> F[Qt界面渲染] ``` ### 二、代码实现 **1. SDK初始化与设备登录** ```cpp // main.cpp #include "HCNetSDK.h" bool initSDK() { if (!NET_DVR_Init()) { qDebug() << "SDK初始化失败!错误码:" << NET_DVR_GetLastError(); return false; } NET_DVR_SetConnectTime(2000, 1); // 设置超时时间 return true; } LONG loginDevice(const char* ip, short port, const char* user, const char* pwd) { NET_DVR_USER_LOGIN_INFO loginInfo = {0}; NET_DVR_DEVICEINFO_V40 deviceInfo = {0}; strcpy(loginInfo.sDeviceAddress, ip); loginInfo.wPort = port; strcpy(loginInfo.sUserName, user); strcpy(loginInfo.sPassword, pwd); loginInfo.bUseAsynLogin = false; LONG lUserID = NET_DVR_Login_V40(&loginInfo, &deviceInfo); if (lUserID < 0) { qDebug() << "登录失败!错误码:" << NET_DVR_GetLastError(); } return lUserID; } ``` **2. 多相机管理与回调处理** ```cpp // videomanager.h class VideoManager : public QObject { Q_OBJECT public: struct Camera { LONG lUserID; LONG lRealHandle; QLabel* displayLabel; }; explicit VideoManager(QObject* parent = nullptr); bool addCamera(const QString& ip, short port, const QString& user, const QString& pwd, QLabel* displayWidget); private: static void CALLBACK realDataCallBack(LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, void* pUser); QMap<QString, Camera> cameras; }; // videomanager.cpp void VideoManager::realDataCallBack(/*...参数*/) { if (dwDataType == NET_DVR_STREAMDATA) { // 获取关联的Qt组件 QLabel* label = static_cast<QLabel*>(pUser); // 转换YUV数据为RGB(海康默认格式) QImage img = convertYUV2RGB(pBuffer, width, height); // 跨线程更新UI QMetaObject::invokeMethod(label, [=]{ label->setPixmap(QPixmap::fromImage(img).scaled( label->size(), Qt::KeepAspectRatio)); }); } } bool VideoManager::addCamera(/*...参数*/) { Camera cam; cam.lUserID = loginDevice(ip.toUtf8().constData(), port, user.toUtf8().constData(), pwd.toUtf8().constData()); NET_DVR_PREVIEWINFO previewInfo; previewInfo.lChannel = 1; // 通道号 previewInfo.dwStreamType = 0; // 主码流 previewInfo.hPlayWnd = nullptr; cam.lRealHandle = NET_DVR_RealPlay_V40(cam.lUserID, &previewInfo, realDataCallBack, static_cast<void*>(displayWidget)); cameras[ip] = cam; return cam.lRealHandle >= 0; } ``` **3. GPU加速渲染(可选优化)** ```cpp // 使用OpenGL纹理渲染 void VideoWidget::paintEvent(QPaintEvent*) { QPainter painter(this); if(!texture.isCreated()) { texture.create(); texture.bind(); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width(), height(), 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); } // 更新纹理数据 texture.setData(QOpenGLTexture::RGB, QOpenGLTexture::UInt8, frameData.data()); // 绘制纹理 QOpenGLVertexArrayObject::Binder vaoBinder(&vao); program.bind(); texture.bind(); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } ``` ### 三、窗口布局示例 ```cpp // mainwindow.cpp void MainWindow::setupCameras() { QGridLayout *grid = new QGridLayout(this); const QList<CameraConfig> cameras = { {"192.168.1.64", 8000, "admin", "12345"}, {"192.168.1.65", 8000, "admin", "12345"}, // 添加更多相机... }; for(int i=0; i<cameras.size(); ++i) { QLabel *display = new QLabel(this); display->setMinimumSize(640, 480); grid->addWidget(display, i/2, i%2); videoManager.addCamera(cameras[i].ip, cameras[i].port, cameras[i].user, cameras[i].pwd, display); } } ``` ### 四、注意事项 1. **线程安全** - 回调函数运行在SDK线程,更新UI需用`QMetaObject::invokeMethod` - 使用互斥锁保护共享数据 ```cpp QMutexLocker locker(&frameMutex); ``` 2. **资源释放** ```cpp void cleanUp() { for(auto& cam : cameras) { NET_DVR_StopRealPlay(cam.lRealHandle); NET_DVR_Logout(cam.lUserID); } NET_DVR_Cleanup(); } ``` 3. **性能优化** - 使用硬件加速:`QSurfaceFormat::setDefaultFormat(...)` - 限制帧率:`NET_DVR_SetDisplayBurst()` - 减少拷贝:使用`QOpenGLTexture`直接渲染内存数据 ### 五、常见问题解决 1. **错误码113**:网络连接超时 → 检查防火墙/IP设置 2. **画面卡顿**:降低分辨率或使用子码流 3. **内存泄漏**:确保每路相机调用`NET_DVR_StopRealPlay` 4. **跨平台问题**:Windows需部署`HCNetSDK.dll`+`PlayCtrl.dll` > 完整项目示例参考:[Qt-Hikvision-MultiCamera](https://github.com/examples/Qt-Hikvision-MultiCamera) [^1]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值