MPEG-4/H.264视频编解码工程实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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”按钮退出应用程序。