图像:VC+DirectShow对视频进行图片处理

现在的图像越来越花巧了,有浮雕、马赛克、相框等特效,看得人眼花缭乱。本来图像特效没什么稀奇的,在PhotoShop等图像处理软件中我们早已见得多了,不过用在视频上就令人感觉有点神奇。我一直都想拥有这些效果,但我的摄像头是很早就买到的,没福气奢望驱动程序给它带来的全新精彩。刚好我学习DirectShow有一段时间了,既为了挑战自己(我从未写过令自己感到满意的程序),也为了检验学习成果,我就下了决心用DirectShow实现这些效果。几经努力,终于有了一些成绩,我完成了其中一些效果,并发现程序可以用在DirectShow支持的影音文件上,又自己把它应用到D3D中去,感觉还不错。

 

  先看看效果吧,以激励斗志。我是对着摄像头广告中的效果图来做程序的,我怕编程的热情像以前那样很快冷却,只留下一堆乱糟糟的代码,我需要它来不断兴奋被代码搞得昏头转向的大脑。

 

  编程的思路是这样的:写一个DirectShow的VideoRenderer Filter用于实时获取图像,之后在用DirectShow连接并使用摄像头、播放视频时用上该Filter,这样就可以实时处理视频的图片并进行显示了。

 

  由于要用到DirectShow,我想在这里说一些关于DirectShow的初级知识,初学或没学过DirectShow的朋友就请耐下性子来听我这个初学者罗嗦一阵子(要学DirectShow的朋友可以看天极的DirectShow相关文章,介绍很详细)。

 

  在新版的DX9 SDK中已看不到DirectShow的影子了(DirectShow发展到了尽头?),不过DirectShow还可用,而且相当有用。听说VC.net自带有DX8 SDK开发文件,但我仍希望您能找到DX8 SDK和VC6。DX8 SDK帮助文件对DirectShow详细的说明和SDK丰富的DirectShow例子对DirectShow开发是大有裨益的。我在开发Filter过程中发现VC.net不能编译通过,出现“InterLockedExchange 重定义”的编译错误,VC6则一切正常,我把这归咎为微软的问题,菜鸟的我无力解决。是了,我只用VC6编写Filter,其它编码使用的是VC.net,VC.net可完成一般的DirectShow编程,我本人更喜欢VC.net的编码环境。

 

  “请问Filter是什么?”这个问题在我脑中很久了,DirectShow的功能是由Filter搭建起来的,但我很久以来都被“Filter”这个词困惑着,直到现在才有些眉目。当然,我学识浅,很可能会说错,请海涵。Filter在影音风暴等软件中称为“滤镜”,在微软中国上一些译文把它译为“筛选器”,五花八门的(看来没有核心技术真的很被动)。而Filter在数字信号处理等专业课程中称为“滤波器”!这下您应该知道一点了吧,DirectShow对视频、音频的处理过程就是数字信号处理的过程,可以把数字信号处理的理论应用于此,微软为此把这个DirectShow部件称为Filter。

 

  再说Sample,我把它译为“采样”,也就是一个数据包,可理解为摄像头摄像或声卡录音时每扫描一次得到的数据、音视频文件每一帧的数据。

开发VideoRenderer Filter
  Filter要做以下工作:接受24bit RGB格式的图片,这由上级Filter肢解视频得到,并把它处理成32bit ARGB图片,之后传给外部函数进行进一步处理。
我要Filter这样工作的理由是:几乎所有的视频Filter都接受24bit RGB格式,不用担心会连接失败;32bit ARGB可以很好地支持MMX加速,如果你会用MMX的话,我在本文中会涉及一点MMX,不过和我水平相同,都是初级的;调用外部函数能提供更多的灵活性,不用费尽心思在Filter中封装图像处理函数,可以在写程序时随能力和水平提高而加入新的处理函数,同时也保证了能够及时处理。
怎么样,Filter要做的很少很简单吧,与此一样,写一个Filter也比想象中的简单,我们一步步地看看。
新建一个简单的DLL项目,设置名称为VR,删除VR.cpp中的DllMain函数,添加VR.h和VR.def两个文件,在VR.def中加入以下代码,以完成函数导出。
LIBRARY VR
EXPORTS
DllMain PRIVATE
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
再做些没有创意的东西 —— Filter注册、类工厂定义等,在VR.cpp中加入,我是从DirectShow的Filter例子中复制,再略加修改得来的。

#include "stdafx.h"
#include "VR.h"
#pragma comment(lib,"strmbase.lib")
#pragma comment(lib,"winmm.lib")
// Setup data
const AMOVIESETUP_MEDIATYPE sudIpPinTypes =
{
&MEDIATYPE_Video, // MajorType
&MEDIASUBTYPE_NULL // MinorType
};
const AMOVIESETUP_PIN sudIpPin =
{
L"Input", // The Pins name
FALSE, // Is rendered
FALSE, // Is an output pin
FALSE, // Allowed none
FALSE, // Allowed many
&CLSID_NULL, // Connects to filter
NULL, // Connects to pin
1, // Number of types
&sudIpPinTypes // Pin details
};
const AMOVIESETUP_FILTER sudVRAx =
{
&CLSID_lwVideoRenderer, // Filter CLSID /**/
L"lwVideoRenderer", // String name /**/
MERIT_NORMAL, // Filter merit
1, // Number of pins
&sudIpPin // Pin details
};
// List of class IDs and creator functions for the class factory. This
// provides the link between the OLE entry point in the DLL and an object
// being created. The class factory will call the static CreateInstance
// function when it is asked to create a CLSID_VideoRenderer object
CFactoryTemplate g_Templates[] = {
{ L"lwVideoRenderer" /**/
, &CLSID_lwVideoRenderer /**/
, CVideoRenderer::CreateInstance
, NULL
, &sudVRAx },
};
int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);
// DllRegisterServer
// Used to register and unregister the filter
STDAPI DllRegisterServer()
{
return AMovieDllRegisterServer2( TRUE );
} // DllRegisterServer
// DllUnregisterServer
STDAPI DllUnregisterServer()
{
return AMovieDllRegisterServer2( FALSE );
} // DllUnregisterServer
extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE, ULONG, LPVOID);
// DllMain
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
{
return DllEntryPoint((HINSTANCE)(hModule), dwReason, lpReserved);
}// DllMain

  经过一番复制后,需要增加一些简单的业务逻辑。我们先来完成Filter的类定义,从CBaseVideoRendeer派生一个新类,重写四个函数就可奠定这个Filter的基本功能,如下,在VR.h中加入。

 
#include <streams.h>
// 回调类定义
class FunCLS
{public: virtual void procFun(BITMAPINFO* pBmpInfo, BYTE* pb){return;};
};
// 回调函数指针定义
typedef void (CALLBACK* pProcFun)(BITMAPINFO* pBmpInfo,BYTE* pb);
// {F81331DB-2E46-43e7-8709-BE57205D8914} Filter的全局标识符
static const GUID CLSID_lwVideoRenderer = 
{ 0xf81331db, 0x2e46, 0x43e7, { 0x87, 0x9, 0xbe, 0x57, 0x20, 0x5d, 0x89, 0x14 } };
// Filter 类定义
class CVideoRenderer : public CBaseVideoRenderer
{
public:
// 创建进程。
static CUnknown * WINAPI CreateInstance(LPUNKNOWN, HRESULT *);
// 构造、释构函数
CVideoRenderer(LPUNKNOWN pUnk,HRESULT* phr);
~CVideoRenderer();
public:
// 检查是否有可以接受格式的数据
HRESULT CheckMediaType(const CMediaType* pmt);
// 设置具体的数据格式,如视频图像的宽、高等
HRESULT SetMediaType(const CMediaType* pmt);
// 递交数据,即显示、呈现数据
HRESULT DoRenderSample(IMediaSample* pMediaSample);
private:
BITMAPINFO m_bmpInfo; // 图片信息
BYTE* m_pCopyBuffer; // 复制缓冲区
UINT m_pixelNum; // 像素点的数目
FunCLS* m_pFunCLS; // 回调类指针
pProcFun m_pPF; // 回调函数指针
};

  我在上面曾提过在Filter中要在接受到新数据时调用外部函数进行处理,因此我定义了一个回调类(我自己称呼的)和一个回调函数指针。这样可以把回调类作为MFC视图类的一个基类,以方便地使用MFC视图类中的成员变量。而同时提供回调函数指针就可以满足同时播放多个视频文件、使用多个摄像头的需要。这是我在使用中感到有必要而后来修改得来的,使Filter的使用具有足够的灵活性。下面看看以上Filter类中函数的具体实现。

//===========================================================
// 创建进程。
CUnknown* WINAPI CVideoRenderer::CreateInstance(LPUNKNOWN pUnk,HRESULT* phr)
{
return new CVideoRenderer(pUnk,phr);
}
//===========================================================
// 构造函数
CVideoRenderer::CVideoRenderer(LPUNKNOWN pUnk,HRESULT *phr) : CBaseVideoRenderer(CLSID_lwVideoRenderer,"lw Video Renderer",pUnk,phr)
{
m_pCopyBuffer = NULL;
m_pFunCLS = NULL;
m_pPF = NULL;
m_pixelNum = 0;
}
//===========================================================
// 释构函数
CVideoRenderer::~CVideoRenderer()
{
if(this->m_pCopyBuffer){
delete [] m_pCopyBuffer;
}
}
//===========================================================
// 检查媒体类型
HRESULT CVideoRenderer::CheckMediaType(const CMediaType* pmt)
{
VIDEOINFO *pvi;
// 只接受视频
if( *pmt->FormatType() != FORMAT_VideoInfo ) {
return E_INVALIDARG;
}
// 只接受 RGB24 格式,即 R、G、B各 1 Byte
pvi = (VIDEOINFO *)pmt->Format();
if(IsEqualGUID( *pmt->Type(),MEDIATYPE_Video) && IsEqualGUID( *pmt->Subtype(),MEDIASUBTYPE_RGB24)){
return S_OK;
}
return E_INVALIDARG;
}
//===========================================================
// 设置媒体类型,获取图像的各种信息(宽、高等具体信息),处理图像要用到
HRESULT CVideoRenderer::SetMediaType(const CMediaType* pmt)
{
VIDEOINFO *pviBmp; // Bitmap info header
pviBmp = (VIDEOINFO *)pmt->Format();
memset(&m_bmpInfo,0,sizeof(BITMAPINFO)); // 清零
m_bmpInfo.bmiHeader = pviBmp->bmiHeader; 
// 改为 32bit,因为我会把它处理成 32bit 的
m_bmpInfo.bmiHeader.biBitCount = 32; 
// 当然,缓冲区大小也变了
m_bmpInfo.bmiHeader.biSizeImage = m_bmpInfo.bmiHeader.biSizeImage * 4 / 3; 
// 开辟新 32bit 图片的缓冲区
if(m_pCopyBuffer){ delete [] m_pCopyBuffer;}
m_pCopyBuffer = new BYTE[m_bmpInfo.bmiHeader.biSizeImage];
m_pixelNum = m_bmpInfo.bmiHeader.biWidth * m_bmpInfo.bmiHeader.biHeight;
return S_OK;
}
//===========================================================
// 处理媒体采样
HRESULT CVideoRenderer::DoRenderSample(IMediaSample* pMediaSample)
{
// 获取采样的数据区指针,即 24bit 图片的数据区指针
BYTE* pb = NULL;
pMediaSample->GetPointer(&pb);
if(!pb){
return E_FAIL;
}
// 加锁!锁住我要操作的数据区,以防处理到一半的时候被打断而造成错误
// 其实就是多线程编程中经常使用的临界区的类形式,
// 利用构造函数和释构函数来进入和退出临界区
// m_RendererLock 是 CBaseVideoRenderer 的成员,继承得来。
CAutoLock cAutoLock(&this->m_RendererLock);
// 把 24bit 图片处理成 32bit
BYTE* pb32 = m_pCopyBuffer; // 指向 32bit 缓冲区的指针
for(UINT i = 0; i < m_pixelNum; i ++){
pb32[0] = pb[0];
pb32[1] = pb[1];
pb32[2] = pb[2];
pb32[3] = 0xff; // 0xff 即 255
pb += 3;
pb32 += 4;
}
// 如果有回调类,进行回调处理
if(m_pFunCLS){
m_pFunCLS->procFun(&m_bmpInfo,m_pCopyBuffer); 
}
// 如果有回调函数,进行处理
if(m_pPF){
m_pPF(&m_bmpInfo,m_pCopyBuffer);
}
return S_OK;
}


至此,一个简单的 Filter 完成了,可以编译成功、用regsvr32.exe 注册并到GraphEdit.exe 中进行测试了。不过如果要在程序中使用的话,您会发现无法设置回调函数或回调类。这个Filter 是如此的无用,除了IBaseFilter 接口的基本功能外我们从它身上得不到任何有用的东西。所以,还得给它写个接口,让我们可以设置一些东西。写接口也不是难事,只要有一个接口的例子,随便谁都可以对照写出一个来,我就抄写了一个。新建一个 IVRControl.h 文件,加入下面代码。

// {244DF760-7E93-4cf0-92F4-DCB79F646B7E} 接口的 GUID
static const GUID IID_IVRControl = {0x244df760, 0x7e93, 0x4cf0, {0x92, 0xf4, 0xdc, 0xb7, 0x9f, 0x64, 0x6b, 0x7e}};
// 接口定义
DECLARE_INTERFACE_(IVRControl, IUnknown)

STDMETHOD(GetBmpInfo) (THIS_ // 方法一:获取图片信息
BITMAPINFO** ppBmpInfo ) PURE;
STDMETHOD(GetPointer) (THIS_ // 方法二:获取缓冲区指针
BYTE** ppb // 缓冲区指针的指针 ) PURE;
STDMETHOD(SetFunCLS) (THIS_ // 方法三:设置回调类
FunCLS* pFunCLS // 回调类指针 ) PURE;
STDMETHOD(SetFun) (THIS_ // 方法四:设置回调函数
pProcFun pPF ) PURE;
};


写完接口后就要实现它,在VR.h 中添加 #include "IVRControl.h",而把接口作为Filter 类的一个基类,像这样:

class CVideoRenderer : public CBaseVideoRenderer, public IVRControl


在CVideoRenderer 类中加入接口函数和询问接口函数:

// 询问接口,一般可以不要的,但这里需要使用接口,也重载了
STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void ** ppv);
// 接口函数
DECLARE_IUNKNOWN; 
STDMETHODIMP GetBmpInfo(BITMAPINFO** ppBmpInfo);
STDMETHODIMP GetPointer(BYTE** ppb);
STDMETHODIMP SetFunCLS(FunCLS* pFunCLS);
STDMETHODIMP SetFun(pProcFun pPF);


再在VR.cpp 中加入上述函数的具体实现代码:

//===========================================================
// 询问接口
STDMETHODIMP CVideoRenderer::NonDelegatingQueryInterface(REFIID riid,void** ppv)
{
CheckPointer(ppv,E_POINTER);
if(riid == IID_IVRControl){
// 返回接口。这里有个细节:返回接口时,Filter 的引用计数会增一,所以外部程序用完接口后也要对它进行释放
return GetInterface((IVRControl*) this,ppv);
}else{
return CBaseVideoRenderer::NonDelegatingQueryInterface(riid,ppv);
}
}
//===========================================================
// 以下为接口函数的具体实现,只是简单的赋值
STDMETHODIMP CVideoRenderer::GetBmpInfo(BITMAPINFO** ppBmpInfo)
{
*ppBmpInfo = &this->m_bmpInfo;
return S_OK;
}
STDMETHODIMP CVideoRenderer::GetPointer(BYTE** ppb)
{
*ppb = m_pCopyBuffer;
return S_OK;
}
STDMETHODIMP CVideoRenderer::SetFunCLS(FunCLS* pFunCLS)
{
m_pFunCLS = pFunCLS;
return S_OK;
}
STDMETHODIMP CVideoRenderer::SetFun(pProcFun pPF)
{
m_pPF = pPF;
return S_OK;
}


不知您注意到了没有:接口其实就是一个虚基类。类在 C++ 等现代编程语言中无处不在,也没什么好惊奇的,只是有利于更好理解。再有一个,看似功能强大的接口可能偏偏很容易实现,它依附于对象,它的复杂可能都隐藏在对象内了。
可以看出在接口定义中也要用到回调类和回调函数指针的定义,所以我把它们连同 Filter CLSID 的定义一起移到 IVRControl.h 文件的开头,使用到此 Filter 时只把 IVRControl.h 这一个文件包含进去就行了。
不错,我们已经一步步、一个个函数的把设想中的 Filter 写出来了,已成功完成了Filter,以 Release 模式把它编译出来足有80多K,用 UPX 压缩后就是30 多K。这样把代码铺出来看,好像蛮多的,不过我在敲代码时一点也不觉得,因为每个函数所做的的确很少,循着逻辑规矩、步步为营地写真的很easy。

你可能感兴趣的:(filter,Class,byte,templates,图像处理,winapi)