2.4 YUV序列图像显示
无论是待编码的YUV视频源文件,还是经编解码后的数据,均需要按照一定频率显示为RGB位图以观察处理效果。RGB位图的高效显示是VC++编程媒体应用程序的重要任务。下面用两个案例展示如何基于VC++2005编程实现YUV420文件的RGB显示。
2.4.1 YUVviewer显示YUV数据
网络上公开的YUVviewerSrc是开源的YUV显示工具,支持多文件的同时播放,以及单帧或多帧的前进及后退的控制。在VC++2005环境下编译YUVviewerSrc时,可能提示以下错误:
error C2440: “static_cast”: 无法从“int(__thiscall CYUVviewerDlg::*)(void)”转换为“AFX_PMSG”
分析可知,控件的事件消息处理函数的返回值在VC++2005内一般定义为void,而打开文件按钮的消息处理函数OnOpenfile的返回值为int,所以修改该消息处理函数的返回类型为void,并取消原来的返回值。
值得注意的是,原始的YUVviewer软件使用Win32函数实现StretchDIBits图像显示,而该函数工作效率较低,且当图像放大显示时,画面质量较差,有“割裂”现象。为此,光盘中chap2_viewer文件夹下的YUVviewerSrc项目增加了使用VFW的DrawDib设备显示的功能。修改过程这里不再详细介绍,详见光盘“chapter2\chap2_viewer\YUVviewer_src\”中的文件。
在显示YUV图像前,首先确认YUV的分辨率大小(Frame Size),然后确认帧率(Frame Rate),选择或取消图像2倍放大显示,“Open File”打开待显示的文件,最后单击“Play”播放YUV文件。打开并播放tempete_cif.yuv文件的结果如图2-21所示。
图2-21 播放tempete_cif文件
播放完毕后,“Close All”关闭当前文件,“Quit”退出应用程序。
2.4.2 DirectDraw显示YUV数据
DirectDraw是DirectX中为图像、视频的低CPU资源占用,功能流畅显示而设计的, DirectDraw充分利用计算机的显卡加速或增强图像显示功能。微软从DirectX 8.0停止了对DirectDraw的更新,DirectDraw的最新版本是7.0。
通常,视频数据格式多为YUV420,因此应用DirectDraw技术实现YUV420格式数据的低CPU占用、清晰、流畅地显示就尤为重要。前面的DirectShow、VFW等技术均是使用CPU编程将YUV420数据转换到RGB格式,再送显。很明显,大分辨率图像的显示显然增加了CPU的额外开销。而DirectDraw的YUV420数据的直接显示则能极大的降低CPU消耗,提高显卡工作效率。本案例就是应用DirectDraw实现YUV420数据的直接显示。本技术特别适合多路视频图像的实时显示应用。
需特别注意,DirectDraw显示与本机的显卡硬件性能是直接关联的,尤其是早期的计算机显卡、当前的某些笔记本电脑可能并不支持YUV420(YV12)显示。因此,本案例可能在有的读者的电脑中无法正常运行,此时,请确认所用电脑的显卡配置。第10章的视频监控中心软件特别考虑到了这一点。
1.DirectDraw概述
用户编程控制DirectDraw主要是通过DirectDraw对象、DirectDraw表面来实现的,其类型分别是LPDIRECTDRAW7和LPDIRECTDRAWSURFACE7。通常用DD表示DirectDraw,DDS表示DirectDrawSurface。下面介绍采用DirectDraw编程时常用到的开发技术。
(1)DirectDraw对象
要使用DirectDraw,必须创建一个DirectDraw对象,它是DirectDraw的核心。用DirectDrawCreateEx函数创建DirectDraw对象,该函数定义在ddraw.h中,它的原型如下:
HRESULT WINAPI DirectDrawCreateEx( GUID FAR *lpGUID, LPVOID *lplpDD, REFIID iid, IUnknown FAR *pUnkOuter);
其中,lpGUID是指向DirectDraw接口的全局唯一标志符(Global Unique IDentify)的指针。lplpDD用来接受初始化的DirectDraw对象。Iid定义为IID_IDirectDraw7,表示要创建IDirectDraw7对象。pUnkOuter必须是NULL。
(2)设置控制级
DirectDraw对象创建成功后,lpDD指向该指针,该对象是DirectDraw接口的最高控制结构,以后的所有操作均由该对象控制。用DirectDraw的SetCooperativeLevel()来设置应用程序对系统的控制。其原型如下:
HRESULT SetCooperativeLevel (HWND hWnd, DWORD dwFlags )
其中,参数hWnd是窗口句柄,使DirectDraw对象与该窗口联系。第二个参数是控制级标志。控制级描述了DirectDraw如何与显示设备及系统发生作用。DirectDraw控制级一般被用来决定应用程序是运行于全屏模式(必须与独占模式同时使用),还是窗口模式。如设置为DDSCL_NORMAL|DDSCL_NOWINDOWCHANGES,表示不允许对DirectDraw应用程序最小化或还原,普通的控制级(DDSCL_NORMAL)表明应用程序将以窗口的形式运行。
(3)创建DirectDraw表面
DirectDrawSurface对象代表一个表面。表面可想象为一张张可供DirectDraw描绘的画布。表面有很多种表现形式,它既可以是可见的主表面(Primary Surface);也可以是作切换用用的不可见的后台缓存(Back Buffer)表面,切换后成为可见;始终不可见的离屏表面(Off-screen Surface),作用是存储图像。其中,最重要的表面是主表面,每个DirectDraw应用程序都必须至少创建一个主表面,一般来说它代表着计算机屏幕。
创建一个表面之前,首先需要填充DDSURFACEDESC2结构,它是DirectDraw Surface Description缩写,即DirectDraw的表面描述,其结构非常复杂,详见Visual Studio中的MSDN开发指南。
表面描述填充后,把它传递给CreateSurface()方法即可创造表面。该方法的的原型是:
HRESULT CreateSurface( LPDDSURFACEDESC2 lpDDSurfaceDesc, LPDIRECTDRAWSURFACE FAR *lplpDDSurface, IUnknown FAR *pUnkOuter);
其中,第一个参数是被填充了表面信息的DDSURFACEDESC2结构的地址;第二个参数是接收主表面指针的地址;第三个参数为保留的NULL。若函数调用成功, lplpDDSurface将成为一个合法的主表面对象。
由于待显示的图像是YUV内存数据,所以需要采用DirectDraw的离屏幕表面,创建过程基本同主表面,具体过程可参见后续的案例。
(4)图像表面传递
在显示内存的图像数据时,首先锁定离屏表面,IDirectDrawSurface7::Lock()实现锁定表面,修改表面内容,然后主表面的方法Blt修改(显卡实现快速拷贝)内容。
函数Lock的原型为:
HRESULT Lock( LPRECT lpDestRect, LPDDSURFACEDESC2 lpDDSurfaceDesc, DWORD dwFlags, HANDLE hEvent );
其中,第一个参数为指向RECT指针,指定将被锁定的表面区域。若为NULL,表示整个表面将被锁定。第二个参数为指向DDSURFACEDESC2结构的地址,将被填充表面的相关信息。第三个参数dwFlags表示锁定的标志。第四个参数为NULL。
函数Blt的原型为:
HRESULT Blt( LPRECT lpDestRect, LPDIRECTDRAWSURFACE7 lpDDSrcSurface, LPRECT lpSrcRect, DWORD dwFlags, LPDDBLTFX lpDDBltFx);
其中,lpDDSrcSurface是源表面的指针,lpDestRect和lpSrcRect分别是目标和源表面的矩形的指针,如果两矩形的大小不一致会自动缩放。dwFlags是标志,如DDBLT_WAIT。参数DBltFx指明特效,通常为NULL。
2.CDirectDraw类
为方便应用程序对DirectDraw的控制和访问,将DirectDraw有关的操作封装在类CDirectDraw中,包括初始化、显示图像和释放资源等。
为使用DirectDraw库,引入头文件ddraw.h及库支持。在DirectDraw.h中,有:
#include <ddraw.h>
在DirectDraw.cpp中,有:
#pragma comment(lib,"ddraw.lib") #pragma comment(lib,"dxguid.lib")
在DirectDraw.h中定义类CDirectDraw:
class CDirectDraw { public: CDirectDraw(); virtual ~CDirectDraw(); // 初始化DirectDraw BOOL InitDirectDraw(HWND hwnd,int width,int height); // 释放DirectDraw资源 void ReleaseDirectDraw(void); // 图像表面传递 BOOL DrawDirectDraw(HWND hwnd,void *buffer); protected: //把图像拷贝到DirectX表面 void CopyToDDraw(void* destination_buffer,void* source_buffer); private: //DirectX DDSURFACEDESC2 ddsd; //DirectDraw表面描述结构体 LPDIRECTDRAW7 lpDD; //DirectDraw对象指针 LPDIRECTDRAWSURFACE7 lpDDSPrimary; //DirectDraw主表面指针 LPDIRECTDRAWSURFACE7 lpDDSOffscreen; //DirectDraw离屏表面指针 LPDIRECTDRAWCLIPPER lpClipper; //DirectDraw裁剪对象 //图像大小 int bitmap_width; int bitmap_height; //显示的源与目标区域 CRect rctSour; CRect rcDest; };
上述定义中,lpDD控制所有的操作,ddsd为各个表面的描述结构体,一个目标表面lpDDSPrimary,一个源表面lpDDSOffscreen,lpClipper裁剪器剪切图像。
(1)初始化DirectDraw
为使用DirectDraw高效显示图像,对DirectDraw做初始化,主要包括两个表面的创建:目标主表面和源YUV离屏表面。
BOOL CDirectDraw::InitDirectDraw(HWND hwnd , int width , int height) { if( !::IsWindow(hwnd) ) return FALSE; // 创建DirectCraw对象 if (DirectDrawCreateEx( NULL, //指向DirectDraw接口的GUID的指针,NULL表示采用默认 (VOID**)&lpDD, //用来接受初始化的DirectDraw对象的地址 IID_IDirectDraw7, //IID_IDirectDraw7,当前版本 NULL)!=DD_OK) //NULL, 保留 { return FALSE; } // 设置DirectDraw控制级 if( FAILED ( this->lpDD->SetCooperativeLevel( hwnd, //与DirectDraw对象联系的主窗口 DDSCL_NORMAL | DDSCL_NOWINDOWCHANGES ) ) )//控制级标志 { return FALSE; } //清空DirectDraw表面描述结构体 ZeroMemory(&ddsd,sizeof(ddsd)); // 填充主表面描述 ddsd.dwSize=sizeof(ddsd); //DirectDraw表面描述结构体大小 ddsd.dwFlags=DDSD_CAPS; //设定DDSURFACEDESC2结构中的ddsCaps有效 ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;//主表面 //1. 创建主表面 if(lpDD->CreateSurface(&ddsd, //被填充了表面信息的DDSURFACEDESC2结构的地址 &lpDDSPrimary, //接收主表面指针 NULL)!=DD_OK) //NULL, 保留 { return FALSE; } //创裁减器 if(lpDD->CreateClipper(0, //现在不用,必须设为0 &lpClipper, //指向剪裁器对象的指针 NULL)!=DD_OK) //NULL return FALSE; //裁减器与显示窗口联系 if( lpClipper->SetHWnd( 0, hwnd ) != DD_OK ) { lpClipper->Release(); return FALSE; } //把裁减器加到主表面 if( lpDDSPrimary->SetClipper( lpClipper ) != DD_OK ) { lpClipper->Release(); return FALSE; } // Done with clipper lpClipper->Release(); //2. 创建YUV表面 ZeroMemory(&ddsd,sizeof(ddsd)); // 清空表面描述体 ddsd.dwSize = sizeof(ddsd); // 离屏表面 ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; // 填充标志 ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT;//宽、高与像素结 构 ddsd.dwWidth=width; //离屏显示的宽 ddsd.dwHeight=height; //离屏显示的高 ddsd.ddpfPixelFormat.dwSize=sizeof(DDPIXELFORMAT); //DirectDraw表面像素格式结构体 ddsd.ddpfPixelFormat.dwFlags =DDPF_FOURCC|DDPF_YUV; // 填充四个字符及YUV标志 ddsd.ddpfPixelFormat.dwFourCC=MAKEFOURCC('Y','V','1','2'); // 四个字符为YV12 ddsd.ddpfPixelFormat.dwYUVBitCount=8; //YUV位宽 // 创建YUV表面 if (lpDD->CreateSurface(&ddsd, &lpDDSOffscreen, NULL) != DD_OK) { return FALSE; } this->bitmap_width=width; //图像的宽 this->bitmap_height=height; //图像的高 // 待显示的图像窗口 rctSour.left = 0; rctSour.top = 0; rctSour.right = ddsd.dwWidth; rctSour.bottom = ddsd.dwHeight; return TRUE; }
上述程序中,首先创建DirectDraw对象,然后设置DirectDraw控制级,创建主表面、裁剪器,创建YUV离屏表面,最后获取待显示图像的窗口大小。
在此需要特别说明,使用DirectDraw显示YV12(YUV420)图像格式时,YUV表面可创建为Offscreen离屏表面或Overlay覆盖表面。离屏表面可创建多个,而覆盖表面只能创建一个。有些笔记本电脑的显卡可能不支持离屏表面,而仅支持覆盖表面。其实,大部分的台式机电脑显卡基本都支持离屏表面。在视频监控中心软件中,通常时多路通道同时解码、显示,所以使用DirectDraw显示YUV420数据的表面创建为DDSCAPS_OFFSCREENPLAIN,即离屏表面。
(2)应用DirectDraw显示图像
显示图像在DirectDraw表现为从一个表面复制到另一个表面,而复制工作是由显卡来完成的,所以CPU占用极低。图像显示过程为:
BOOL CDirectDraw::DrawDirectDraw(HWND hwnd, void * buffer) { HRESULT ddRval; if( buffer==NULL) return FALSE; // 获取目标客户区坐标 GetClientRect(hwnd,&rcDest); // 把窗口坐标转换为屏幕坐标 ClientToScreen(hwnd, (LPPOINT)&rcDest.left); ClientToScreen(hwnd, (LPPOINT)&rcDest.right); // 查询锁定离屏表面 do { ddRval = lpDDSOffscreen->Lock( NULL,// 指向某个RECT的指针,它指定将被锁定的页面区域。 //如果该参数为NULL,整个页面将被锁定 &ddsd,// DDSURFACEDESC结构的地址,将被填充页面的相关信息 DDLOCK_WAIT | DDLOCK_WRITEONLY,//锁定标志 NULL); //NULL }while(ddRval==DDERR_WASSTILLDRAWING);// 块传送器正忙,继续查询 // 传送完毕 if(ddRval != DD_OK) return 1; // 复制待显示图像到主表面内存 CopyToDDraw( (LPBYTE)ddsd.lpSurface , buffer); // 解锁离屏表面 lpDDSOffscreen->Unlock(NULL); // 将离屏表面的YUV源图像(rctSour)画到主表面的rcDest目标区 this->lpDDSPrimary->Blt( &rcDest , this->lpDDSOffscreen , rctSour, DDBLT_WAIT, NULL ); return 1; }
上述过程中,由于显示窗口可能有移动或变化,所以在显示前获取当前窗口的句柄。循环以锁定Offscreen,然后将待显示的图像Buffer复制到lpSurface,解除锁定,利用方法Blt显示YUV图像。
(3)释放DirectDraw资源
系统出错或退出DirectDraw程序时,销毁申请的资源。
void CDirectDraw::ReleaseDirectDraw( void ) { if(this->lpClipper){ //释放裁剪器 this->lpClipper->Release(); this->lpClipper=NULL; } if( this->lpDDSOffscreen ){//释放DirectDraw离屏表面 this->lpDDSOffscreen->Release(); this->lpDDSOffscreen=NULL; } if( this->lpDDSPrimary ) {//释放DirectDraw主表面 this->lpDDSPrimary->Release(); this->lpDDSPrimary=NULL; } if(this->lpDD){ //释放DirectDraw对象 this->lpDD->Release(); this->lpDD=NULL; } }
上述代码实现对已经打开或申请的内释放或销毁,确保没有内存泄漏。
3.DirectDraw案例设计
上述的CDirectDraw类为设计图像显示提供了方法,下面设计一个简单的基于DirectDraw的YUV文件显示案例。
Step 1 创建对话框应用程序
启动VC++2005开发环境,然后根据向导创建一个基于对话框的应用程序,项目名称为“YUVddraw”。项目代码详见光盘chapter2\chap2_ddraw\YUVddraw。
Step 2 为项目添加三个按钮控件和一个图像控件,ID设置见表2-5。
表2-5 YUVddraw控件
Step 3 向项目中添加文件DirectDraw.cpp,DirectDraw.h。
Step 4 为控制图像大小及播放,定义、初始化全局及局部变量。在YUVddrawDlg.h中添加:
#define CHAN_SUM 1 //图像路数,修改该宏启用多路显示 #define XDIM 352 //待显示图像的宽度 #define YDIM 288 //待显示图像的高度
YUVddraw项目默认支持CIF大小的图像显示,用户可以根据需要修改;项目支持一路图像显示。
在类CYUVddrawDlg中添加成员变量:
CDirectDraw*m_pDDraw[CHAN_SUM]; //DirectDraw对象指针 CString m_FileName[CHAN_SUM]; //YUV文件路径及名称 BYTE*m_pYUV[CHAN_SUM]; //YUV数据 CFile*m_pFile[CHAN_SUM]; //YUV文件指针 CWnd*m_pStaticVideo[CHAN_SUM]; //图像显示窗口
在OnInitDialog中添加上述变量的初始化:
//TODO: 在此添加额外的初始化代码 // 清空变量 for (UINT chan=0; chan<CHAN_SUM; chan++) { m_pDDraw[chan]=NULL; m_pYUV[chan] =NULL; m_pFile[chan] =NULL; m_pStaticVideo[chan]=NULL; } // 获得图像显示窗口的句柄 m_pStaticVideo[0]= GetDlgItem(IDC_PICTURE_WINDOW);
上述代码实现所有通道的变量初始化,并获得显示窗口的句柄以方便在打开文件时显示第一帧图像。
Step 5 对按钮的单击事件添加消息处理
双击“Open File”,实现打开文件。
void CYUVddrawDlg::OnBnClickedOpenfile() { UINT chan = 0; //默认仅有一路图像 // YUV文件名称滤波器 CString strFilter; strFilter = "YUV File (*.yuv; *.cif) | *.yuv; *.cif|"; strFilter += "All File (*.*) | *.*|"; //打开文件对话框 CFileDialog dlg(TRUE, NULL, NULL, OFN_PATHMUSTEXIST|OFN_HIDEREADONLY, strFilter, this); if (dlg.DoModal() == IDOK) { m_FileName[chan] = dlg.GetPathName();//获取当前YUV文件的路径 // 非第一次打开,则释放所有资源,然后重新创建 if (m_pDDraw[chan]) { KillTimer(chan); //销毁定时器 m_pFile[chan]->Close(); //关闭当前YUV文件 m_pDDraw[chan]->ReleaseDirectDraw(); //释放当前的DirectDraw delete m_pDDraw[chan]; m_pDDraw[chan]=NULL; //删除DirectDraw对象并清空 delete m_pFile[chan]; m_pFile[chan]=NULL; //删除CFile对象并清空 free(m_pYUV[chan]); m_pYUV[chan]=NULL; //释放内存并清空 } // 创建DirectDraw对象 m_pDDraw[chan] = new CDirectDraw; // 根据视频显示窗口及图像大小初始化DirectDraw对象 m_pDDraw[chan]->InitDirectDraw(this->m_pStaticVideo[chan]->GetSafeHwnd(),XDIM,YDIM); // 创建CFile,便于操作文件 m_pFile[chan] = new CFile(); // 分配内存,保存YUV数据 m_pYUV[chan] = (BYTE *)malloc(XDIM*YDIM*3/2); // 以只读方式打开YUV文件 if(!m_pFile[chan]->Open(m_FileName[chan], CFile::modeRead|CFile::shareDenyNone)) { AfxMessageBox(_T("Can't open input file")); return; } // 读取第一帧图像送显 if (m_pFile[chan]->Read(m_pYUV[chan],XDIM*YDIM*3/2)==(XDIM*YDIM*3/2)) m_pDDraw[chan]->DrawDirectDraw(m_pStaticVideo[0]->GetSafeHwnd(),m_pYUV[chan]); } }
上述过程中,首先启动文件对话框并读取选定的路径及名称,若当前正在播放图像,则销毁所有资源,包括定时器及其他资源。创建新的DirectDraw对象指针,并根据图像分辨率及显示窗口句柄初始化DirectDraw对象,申请内存以存储YUV文件,创建文件对象指针并打开选定的YUV文件。最后读取第一帧图像显示。
双击“Play File”,实现播放文件。
void CYUVddrawDlg::OnBnClickedPlayfile() { //TODO: 在此添加控件通知处理程序代码 UINT chan = 0; // 恢复到文件开头 m_pFile[chan]->SeekToBegin(); // 启动定时器,默认帧率25f/s SetTimer(chan,1000/25,NULL); }
上述过程中,首先将文件指针放置于开始位置,然后启动定时器,周期性地读取YUV数据。
双击“Exit App”,实现退出程序。
void CYUVddrawDlg::OnBnClickedExitapp() { //TODO: 在此添加控件通知处理程序代码 for (UINT chan=0; chan<CHAN_SUM; chan++) { if (m_pDDraw[chan]) { KillTimer(chan); //销毁定时器 m_pFile[chan]->Close(); //关闭当前YUV文件 m_pDDraw[chan]->ReleaseDirectDraw(); //释放当前的DirectDrawddraw delete m_pDDraw[chan]; m_pDDraw[chan]=NULL; //删除DirectDraw对象并清空 delete m_pFile[chan]; m_pFile[chan]=NULL; //删除CFile对象并清空 free(m_pYUV[chan]); m_pYUV[chan]=NULL; //释放内存并清空 } } SendMessage(WM_SYSCOMMAND,SC_CLOSE,NULL); //发送关闭的系统命令消息 }
在退出系统前,首先关闭定时器,销毁所有通道的有关资源,并清空,最后发送系统关闭命令。
Step 6 编译、运行YUVddraw,显示YUV420文件,如图2-23所示。
图2-23 YUVddraw图像显示应用程序
首先单击“Open File”按钮,定位待显示的CIF大小的YUV文件,“stefan.yuv”,然后单击“Play File”按钮播放YUV数据,播放完毕,单击“Exit App”按钮退出应用程序。