VC+DirectShow对视频进行图片处理(转)

现在的图像越来越花巧了,有浮雕、马赛克、相框等特效,看得人眼花缭乱。本来图像特效没什么稀奇的,在PhotoShop等图像处理软件中我们早已见得多了,不过用在视频上就令人感觉有点神奇。我一直都想拥有这些效果,但我的摄像头是很早就买到的,没福气奢望驱动

程序 给 它带来的全新精彩。刚好我学习DirectShow有一段时间了,既为了挑战自己(我从未写过令自己感到满意的程序),也为了检验学习成果,我就下了决心 用DirectShow实现这些效果。几经努力,终于有了一些成绩,我完成了其中一些效果,并发现程序可以用在DirectShow支持的影音文件上,又 自己把它应用到D3D中去,感觉还不错。

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


(图一:几种 效果 ) (图二: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-2E4**3e7-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。

为方便使用DirectShow而写一个封装类

 

  如果您使用 DirectShow 有一阵子,您一定会选择写一个类来封装 DirectShow,谁也愿意只调用“PlayMovie”这些只用传入文件名就能播放的函数来播放文件,而不想每次都作一大堆初始化和使用一大堆对象,而且类可以方便的移动到不同项目中。

我在写这个类是遇到一个问题:怎样使用自写的 Filter,先注册再使用还是不必注册直接手工载入呢?想到绿色软件是不往注册表中添加多余信息的,而且要使程序能在进行重装系统等 导致注册表信息丢失的操作后仍无需重装而正常运行,因此我选择了手工方式。这样做也不难,只是模拟一遍 COM 的载入,也就是 CoCreateInstance 工作流程的模拟。我不知道高手们是怎样做的,这里只是我的做法,流程如下:用 LoadLibrary 载入VR.dll,使用 GetProcAddress 查找 GetClassObject 函数的地址,再调用 GetClassObject 得到类工厂,然后用类工厂询问并得到 Filter。

先看一个可以从已载入的 dll 得到对象的函数:
 

// 定义 DllGetClassObject 函数的指针

typedef HRESULT (CALLBACK *lpDllGetClassObject)(REFCLSID,REFCLSID,void**);
HRESULT lwGetClassObject(HMODULE hLib,const CLSID& clsid,const CLSID& riid,void** ppv)
{
// 装入和卸载dll由用户执行,以免后来忘记了卸载dll
HRESULT hr;
if(!hLib){
// dll没有装载成功
hr = E_FAIL;
return hr;
}
lpDllGetClassObject lwDGCO = NULL;
lwDGCO =(lpDllGetClassObject)GetProcAddress(hLib,"DllGetClassObject");
if(!lwDGCO){
// 查找函数失败
hr = E_FAIL;
return hr;
}
IClassFactory* pCF = NULL;
hr = lwDGCO(clsid,IID_IClassFactory,(void**)&pCF);
if(!pCF){
// 获取类厂失败
return hr;
}
hr = pCF->CreateInstance(NULL,riid,ppv);
if(!ppv) hr = E_FAIL;
pCF->Release();
return hr;
}

我在类中作了如下这些定义:

// 类成员的定义,要包含“IVRControl.h”文件

IGraphBuilder* m_pGB; // Filter Graph
IBaseFilter* m_pFVR; // 自写的 Filter
IVRControl* m_pVRControl; // 接口
HMODULE m_hLib; // dll 库的句柄

在类的构造函数中载入 dll:

m_hLib = LoadLibrary("VR.dll"); // 载入 dll,“VR.dll”要和程序在同一目录

之后在类的成员函数 InitDS 中做获取 Filter 等工作:

HRESULT hr;
// 创建 Filter Graph
hr = CoCreateInstance(CLSID_FilterGraph,NULL,CLSCTX_INPROC,IID_IGraphBuilder,(void**)&m_pGB);
if(FAILED(hr)){
return hr;
}

// 调用上面的函数从已载入的 dll 中获取自写的 Filter

hr = lwGetClassObject(m_hLib,CLSID_lwVideoRenderer,IID_IBaseFilter,(void**)&m_pFVR);
if(FAILED(hr)){
return hr;
}

// 询问接口

hr = m_pFVR->QueryInterface(IID_IVRControl,(void**)&m_pVRControl);

if(FAILED(hr)){
return hr;
}

// 把 Filter 加入 Filter Graph

hr = m_pGB->AddFilter(m_pFVR,L"VideoRenderer");

if(FAILED(hr)){
return hr;
}

最后在类的释构函数中卸载dll。本来我在一本2000年版的《COM 精髓》中看到要先调用 dll 的 DllCanUnloadNow 函数确定是否应卸载才实行卸载的,不过试验中,特别是使用多个类播放几个不同文件时发现就这样直接卸载就行,用 DllCanUnloadNow 作为卸载条件反而不能正确卸载。

if(m_hLib){
// 确认成功载入了 dll
FreeLibrary(m_hLib);// 卸载 dll
}

以上的代码有点乱,请对照流程耐心的看一下,我已作了详细的注释,应该不难看懂。完成这些后,要播放视频文件只需 m_pGB->RenderFile(L”c://sample.wmv”,0); 就可以建立“Filter Chain”了,再用 m_pMC->Run(); 就可开始播放(m_pMC 为 IMediaControl对象),完全是最一般的 DirectShow 操作,摄像头控制也一样,我不多贴代码了,您可到我拙劣的程序中看。这儿要提的是接口,在上面代码中如无意外我们已经获得了 Filter 的接口,不过到目前为止这个接口只通到此 DirectShow 类中,也就是只能在此类中使用,我们还得把它导通到类外以便设置函数,它的传递代码如下,仅以 SetFun 为例:

HRESULT CDSControl::SetFun(pProcFun pPF)
{
if(m_pVRControl){
return m_pVRControl->SetFun(pPF);
}
return E_FAIL;
}

图像处理

  在我的程序中图像处理函数是作为 DirectShow 封装类一部分的,我认为这样便于移动和使用。没有连着上面的 DirectShow 类而另外写标题是因为我觉得有必要把它提到重要位置。
 

在程序完善阶段您的工作基本就在这里了,除了花心思构造D3D环境外几乎所有效果都要在这里实现,水平高下也体现于此。 在网上可以找到很多图像特效的代码和解说,我结合编程过程再说说。

1. 访问缓冲区的麻烦。

这是最麻烦的,二维图像在这里以一个连续的一维缓冲区呈现,您要靠一个指针去访问它,怎么办呢?先弄懂 Pitch,例如 32bit ARGB 图像,每个象素就占用 4 Byte 内存空间(1 Byte = 8 bit),对于宽度为 20 像素的图像,它的 Pitch 就是 80 Byte,即每一行占用的内存。按 x 、y 坐标就有如下公式(按 Byte 计算):

B:y * Pitch + x * 4
G:y * Pitch + x * 4 + 1
R:y * Pitch + x * 4 + 2
A:y * Pitch + x * 4 + 3

可以看出在内存中是按 BGRA 存储的,我不明白为什么这样,可能可以从计算机的内存存储方式找到答案。上述公式计算多,效率较低,在实际使用中应适时作有益的改变。

2. 浮雕。

到目前为止我在网上找到的几篇文章都说把一点的值减去其右下角点的值再加上128就行了。为什么要减去右下角的点呢?为什么要加上128呢?原来浮雕是 要把图像的变化部分突出显示出来,而把相同部分淡化,所以用一点减去其邻域任意点都可以达到这个目的,倒也不一定是右下角的点,包围着它的八个点都可以, 甚至可以选择减去更远的点,只要规则明确、效果好就行。在相减后点的 RGB 值都减小了,大多接近黑色,黑乎乎一片的看不出什么来,一点也不像浮雕,所以要给它们都增加一个相同的亮度,通常加上128,其它的值,例如64、 100,当然也行,一切都以实际效果为准。说到效果,上面所说的RGB相减会造成浮雕有一些色点,解决方法是计算两点的亮度之差,RGB都赋值为亮度差, 画面就没色点了,因为已经变成灰度图了。亮度公式是 Brightness = 0.3 * R + 0.6 * G + 0.1 * B,其中G 的比重最大,可以近似的用 G 作为亮度,在RGB各自的分量图中也可以明显看出 G 分量的图最亮,简单的把 G 的值赋给 R 和 B 就得到灰度图了,这可以减少计算,提高速度。后来我还看到这样的话句,“用3 * 3 的小块做的浮雕效果更好”,不过我不知道怎么用,可能这样就可以实现 PhotoShop 那样更好的浮雕效果。

原理是这样了,到了编程却是另外一回事:能够把规则、数学公式转换为程序也是能力的一种体现。如果要减去右下角的点,那么最右一列和最后一行是要特殊处 理的,否则肯定会发生内存访问错误,想一想就知道为什么;如果要减去左边的点,第一列也要特殊处理,请问第一列的点到哪里找它左边的点呢?不要小视此问 题,它会令你访问内存时遇到一些问题。

3. 铅笔画

铅笔画原理和浮雕差不多,也是亮度相减,认为变化大的是边缘,然后设置一个阀值,例如差值大于8,则把该点设为黑色(0,0,0),要不设为白色(255,255,255)。阀值、色彩都可任意设置,没人要您拘束就不要忸忸怩怩的不敢改动。

按照此方法得到的效果实在不怎么样,可惜我不是研究图像的料,对数据的处理能力很差,同样一幅在专家手中可以玩出很多花样的图片,沦落到我手上也只能饮 恨屈膝投降无奈了。这是我看了一些图像处理方面资料和书籍所发的呆叹,图像处理实在太精深了,既要数学、物理知识雄厚,又要脑子灵活能东移西就把各种知识 综合运用,不然就只好望洋兴叹。

4. 加亮、对比度等。

首先悲痛的说明,我曾努力的要实现色度、饱和度的调 整,知道是要把 RGB 转换成 HLS 之类的颜色空间才能实现,也找到了一些它们之间转换的说明和转换函数,可惜看不明白,或者说那些材料根本不打算让我明白!这不单是气话,而且事实,我真的 十分气愤:怎么能够在前面铺了一大堆“效果图”说了一大堆废话然后给个只有几行无大用的注释的代码就可以呢?!尽管如此愤概,我还是乖乖的抄了程序,希冀 能发挥作用,结果却是失望:不仅效率低下,而且在调整了饱和度的同时使图像出现不协调的彩色方块。由于不知道原理,无法改动,于是我放弃了它。 调整亮度很简单,例如要加亮10,把RGB 都加上 10 就可以了,减亮就减10。

对比度调整也不难,书上说是要令亮点更亮、暗点 更暗,好像是要找出亮点来增亮、找出暗点加暗,其实不然,把所有点都乘以一个数,把亮暗点的差距拉大或减小就能调整对比度了。把图像原来的对比度定为 1,要增大对比度就调整为 1.3 、2 等大于1 的数,把每个点的 RGB 都乘以它,就行了,要降低就把数值设为 0 至 1 的数。只要注意保持 RGB 的值在 0 ~ 255 中即可。

5. 马赛克

马赛克效果就是把图像分解成 m * n 个小块 或长宽为 x 、y 的小块,用小块内的某点颜色作为整块的颜色,通常用左上角的颜色。

动起手编程会很麻烦,要定位到每小块的左上角,才能改变块内的颜色,因此要用很多循环,我在代码中就用了12个循环!除此,还有逻辑麻烦,拿分成宽高为 x、y 的小块这种情况为例,您不能保证图像的长宽刚好都是 x、y的倍数,很多时会余出一些“边角料”,这就是麻烦,不可能舍弃它们不进行处理,因为很影响效果。因此如图所示,要先处理蓝色的倍数部分,再处理绿色 的宽度上余下部分,处理红色的高度上余下部分,还有黄色的宽高夹缝的小块。



除了这种长方形的处理,还可以试验上图菱形等形状,当然,您要付出很大的劳动,而我没能做到这些。

6. MMX。

记得在上面我说过会在文章中涉及一点MMX,不妨在这里涉及。在 VC 中可以镶入汇编使汇编变得很容易,完全不是纯汇编代码所能相比的,所以不用怕汇编,可以先用 C 语言写出实现代码,再用汇编“翻译”过来。如果译不出来,更加可以把代码中断一下,让 VC 反汇编,看 VC 的汇编代码,再行改进,为什么不行呢,有人用枪指着您么?记住哦,如果没办法改进就放弃汇编,不要做多余的事。其实要用 MMX 也不一定非用汇编不可,VC 也提供了 MMX 的 C++ 封装,学习后可用它,我则懒于学习。

MMX 最大的好处是可以自动保证处理的值范围为0 ~ 255,节省判断,而且MMX寄存器是64位的,一次可处理32bit图像的两个点。其它的我也不太懂,您可参考相关资料。

下面列出浮雕效果代码,它是减去右边点的,由于不进行行列判断,每行最后一点减去的是下一行的首点。
 

__int64 Mask = 0x8080808080808080; // 0x80 = 128,就是亮度的增加值

UINT a = bmpBufferLen >> 3; // 缓冲区长度(按 BYTE 计算)除以 8(两个点的大小),计算要循环的次数

_asm{

mov esi,pIn; // 要处理的缓冲区指针

mov edi,pOut; // 结果缓冲区指针

mov eax,a; // 循环次数

dec eax; // 循环次数减一,因为最后两点没法减,可以在后面特殊处理,这里不作处理

movq mm1,Mask; // 增加值,movq 是 MMX 的专用汇编指令,请找资料看

_loop: // 循环

mov ecx,esi; // ecx 存储右边点的指针

add ecx,4; // 只加 4 就跳过一点到右边点了

movq mm0,[esi]; // 移动要处理的两点的值到 MMX 寄存器

movq mm2,[ecx]; // 移右边两点的值

psubusb mm0,mm2; // 相减

paddusb mm0,mm1; // 加上增加值

movq [edi],mm0; // 移到结果缓冲区

add esi,8; // 移动到下两点

add edi,8; // 同上

dec eax; // 循环计数减一

jnz _loop; // 不为零就继续循环

emms; // 结束 MMX 使用

}

7.来点高级的,用摄像头控制鼠标!

看着这个有点神奇吧,其实比什么都要简单。先做好“硬件准备”:把摄像头如图摆放,镜头下方放张白纸以使图像中物体界限分明。



软件方面,把图片作阀值处理—— B 值大于 128 的设为黑色,其它的设为白色。因为白纸的作用,您的手或其他物体会在阀值图中显示为白色,如下图,再找出图中红点,即第一点白点的在图中位置(x,y),再把图的坐标影射到屏幕坐标就行了。

 

下面要讨论具体做法。先解决图像坐标问题。我获取第一点白点的程序如下:

void CDSControl::GetMousePos(BYTE* pb,int *xPos,int* yPos)
{
int x,y;
BOOL mouseFound = FALSE;
for(y = 0; y < m_bmpHei; y ++){
for(x = 0; x < m_bmpWid; x ++){
if(pb[0] == 255){ // 因为白色为(255,255,255),判断一个255 即可
pb[2] = 255; // 设为红色,别忘了 BGRA 的内存排列方式
pb[1] = pb[0] = 0; //
// 计算坐标
*xPos = ScreenWid - x * ScreenWid / m_bmpWid;
*yPos = y * ScreenHei / m_bmpHei;
mouseFound = TRUE;
break;
}
pb += 4;
}
if(mouseFound){
break;
}
}
}

可以看出我是从所得的图像缓冲区的第一点开始检索的,这也是摄像头扫描 CCD 的顺序,看上面右边的图,那是右手的图像,可以推想出摄像头的扫描顺序如下图所示:


(摄像头扫描坐标) (与屏幕坐标(蓝)相对的摄像头坐标(红))

而相对于屏幕坐标很容易得到上面右图,说明屏幕坐标以左上角为原点,x的正方向为右,而摄像头坐标以右上角为原点,x的正方向为左,也就是说我按顺序寻找所得到的坐标值是这样的:y 与屏幕坐标 y 相符,x 则与屏幕坐标 x 刚好相反。因此推算出鼠标位置应该是:

*xPos = ScreenWid - x * ScreenWid / m_bmpWid;
*yPos = y * ScreenHei / m_bmpHei;

其中 ScreenWid 、ScreenHei 分别是屏幕的宽和高,用 ScreenWid = GetSystemMetrics(SM_CXSCREEN) 和 ScreenHei = GetSystemMetrics(SM_CYSCREEN) 得到,而m_bmpWid、m_bmpHei 当然是图片的宽和高了。得到鼠标坐标后再用 SetCursorPos 设置即可,只不过鼠标晃动会比较厉害,这与图像噪音有关,可能先做个柔化处理会好一点,但鉴于摄像头的摄像质量,我不想作无谓的挣扎,您不会真的想用摄像 头代替鼠标吧?!

  在使用前应该调整二值图的阀值,使整个图都变成黑色,保证能正确滤除干扰,不然在按下“鼠标控制”按钮后您的鼠标就不会听话,您会无法控制好它。请问没有鼠标的帮忙您将如何关闭程序?对了,“Alt + F4”,别忘了,否则您得硬着动手把摄像头拔掉!!

可以说控制鼠标真的很容易实现,不过效果出奇的不错,这种好像无影无踪的控制方式相当令人惊奇,记得我的大哥看程序时对我前面的图像处理没有一丝反应, 看到这个却大大的惊奇!呵呵。如果您有兴趣的话可以在此方面做更多的试验,例如可以把手裁剪出来,让它参加拨动一个小球等游戏,只要您的几何过关、有毅力 就可以实现。

8.更实用的数字减影技术
 

   

    请看上面三幅图,左边的是先存储一幅背景图然后把手放到摄像头前摄像,用摄到的图片减去背景图得到的图;中图是根据左图把手的颜色设为原来颜色得到 的;右图是不断用新图片减去上一幅图得到的手移动痕迹图。这充分显示了数字减影的功用:能够从背景分解物体和侦测到物体的运动。

如此说来此技术在安保方面的应用会很突出,像上面右图那样不断减去上一幅图片,当减影后得到的图片差别大于某一程度的点多到一定数值时就说明有情况发生,这时候就提醒保安工作,弥补保安的人为失误,也可以在此时启动录像录取有价值的情况。请看程序:

void CDSControl::DNS(BYTE* pIn,BYTE* pReduce,BYTE* pOut)
{
// pIn 新图的数据区指针,pReduce 背景指针,pOut 存储区指针
if(!pReduce) return; // 没有背景图就不处理
int differentPoint = 0;
for(int i = 0; i < m_bmpBufferLen; i ++){
// m_bmpBufferLen 为数据区长度
pOut[i] = abs(pIn[i] - pReduce[i]); // 相减,取差值的绝对值
if(pOut[i] > 32){
// 相差大于 32 就认为是不同的点,此值因摄像头而异,与噪音有关,请自行试验
differentPoint ++; // 不同点增加
pOut[i] = pIn[i]; // 把不同点赋回它的颜色 }
if(differentPoint > 200){
// 不同点大于 200 就认为有情况,应适当改变
// 调用警报等……
}
}
}

当然了,我可舍不得整天整夜开着电脑守着我睡觉,只是试验这项技术获取了却罢了。

9. 广阔的图像处理天地

因为写程序的需要,也因为浓厚的兴趣,我在此段期间找了不少图像处理的资料,不过正如前面说过的,图像处理需要数学,大多资料都有一大堆公式,看不看得 懂就得看您的造诣了。虽然我看不懂那些公式,但我也得到了很多有益的启示,它们是我从更多更新的角度去看待图像,改变了我的思维方式,例如图像是平面的, 但可以把其RGB 分量看作是高度,使图形呈现“立体模式”,从而可以对它应用立体几何的方法。

看看我理解的线性放缩吧,这也是下面 D3D 应用的铺垫。


如上图,很容易写出此直线段的方程 y = 2 * x (0<= x <= 10)。我不知道您是怎么理解此方程的,我自己认为以前一点也不理解“映射”这个概念,现在从线性放缩中明白到:x 被映射到 y 上,长度被拉长了 2 倍。这和单单知道方程是两回事,我认为这比原来理解要好。利用这个映射就可以把 11 个像素点(下标为 0 ~ 10)的图像放大为 21 个像素点大小,使用下面程序:

for(y = 0; y < 21; y ++){
x = y / 2;
newPicture[y] = oldPicture[x];
}

就这样把图像的宽度扩大后再扩大高度就可完成整个图像的放大,缩小也一样,把直线段的斜率减小就行。直线段代表线性放大,那么抛物线等曲线呢,分段曲线呢?它们都可以实现放缩,因为都是 x 到 y 的映射,曲线代表的是映射规则。呵呵,都怪中学没学好!

在实际放缩时不可能都除得整数,这时就有两种解决方法,一是插值,上面的右图就是线性插值的示意图,二是取整数部分,也就是最近点法,这两种方法的效果和运算量大家都明瞭,自不必我说。 此放缩原理还可以应用于灰度拉伸等方面,对比度增减就是一个例子。

现在摄像头热门的人脸跟踪也是应用图像处理的,其它高级的物体识别、运动分析等都是基于图像处理,其应用前景十分广阔,您足可投身畅游其中,只要您有足够的兴趣和知识。使用图像处理时您是否有这样一个想法:软件让硬件价值倍增!

应用到D3D中去

  平面的图像、影片看的多了,我们不妨到3D 环境中看看影片。

我不会在这里介绍 D3D,您要学习它就得自己找资料,这里只是讲在 3D 环境中播放的关键—— 图片到纹理。

3D 纹理有一个特点:宽高都必须是 2 的倍数。您是知道的,通常影像都是 320 * 240 等大小的,把这个宽高传入创建得到的纹理却是 512 * 256 大小的。所以非得把影像图片拉伸到纹理不可,不然您得到的纹理会有一片黑色,谈不上美观,尽管我的审美能力让我不觉得黄金分割很美丽,但我敢肯定这样的纹 理很丑陋。在我的程序中利用了最近点法把图像拉伸为纹理大小。不过这样也引起图像宽高比改变,造成一定程度的扭曲,您可自写个程序把图像保持宽高比拉伸。 请看看如何动态改变纹理。
 

HRESULT d3d::SetTex(BYTE* pb)
{
if(!pTexture) return E_FAIL; // 纹理创建不成功
if(!pb) return E_FAIL; // 指针错误
// 锁定纹理
D3DLOCKED_RECT d3dlr;
if (FAILED(pTexture->LockRect(0, &d3dlr, 0, 0)))
return E_FAIL;
BYTE* pTexBits = (BYTE*)d3dlr.pBits; // 取纹理数据区指针
UINT texPitch = d3dlr.Pitch; // 纹理的 Pitch
UINT bmpPitch = bmpWid * 4; // 图片的 Pitch

float xStep = float(bmpWid - 1) / float(texWid - 1); //
float yStep = float(bmpHei - 1) / float(texHei - 1); //

BYTE* pNewBits = pTexBits;
BYTE* pOldBits = pb;

BYTE* pNewPixel;
BYTE* pOldPixel;
// 最近点放大
for(int y = 0; y < texHei; y ++){
pOldBits = pb + int(yStep * y) * bmpPitch; // 定位 y
pNewBits = pTexBits + y * texPitch;
for(int x = 0; x < texWid; x ++){
pPixel = pOldBits + 4 * int(xStep * x);// 定位 x
pNewPixel = pNewBits + 4 * x;
pNewPixel[0] = pOldPixel[0];
pNewPixel[1] = pOldPixel[1];
pNewPixel[2] = pOldPixel[2];
pNewPixel[3] = 255;// 纹理的 alpha 值,如果启用透明,可更改实现透明效果
}
}
// 解锁纹理

if (FAILED(pTexture->UnlockRect(0)))
return E_FAIL;
return S_OK;
}

其中 pTexture 是专为影像而设置的纹理,其他不明来历的变量也是 d3d 类的成员,在下面函数中被赋值。

HRESULT d3d::CreateTex(int wid,int hei)
{
// 根据传入的宽高创建纹理
if(FAILED(D3DXCreateTexture(this->m_pd3dDevice,wid,hei,1,0,D3DFMT_A8R8G8B8,D3DPOOL_MANAGED,&pTexture))){
return E_FAIL;
}

// 纹理描述

D3DSURFACE_DESC ddsd;
if ( FAILED(pTexture->GetLevelDesc( 0, &ddsd ) ) ) {
return E_FAIL;
}

// 核对纹理格式,规定为 A8R8G8B8 的 32bit ARGB 格式

if(ddsd.Format != D3DFMT_A8R8G8B8){
pTexture->Release();
pTexture = NULL;
return E_FAIL;
}
texWid = ddsd.Width; // 纹理宽
texHei = ddsd.Height;// 纹理高
bmpWid = wid; // 图片宽
bmpHei = hei; // 图片高
return S_OK;
}

我不想每次使用纹理时创建一个新的,每次渲染后就释放它,这会影响性能,所以在使用中先在恰当的地方,例如按下播放按钮后就创建纹理,然后在有新影像图 片到来时更新纹理。我用的是Direct3D8,并非Direct3D9,因为我的 GF4(我很想它消失了,可叹袋中空空如也)运行 D3D9 很慢,简单的场景用 D3D8 也足够了。附加一句,美妙的场景可使影片播放增色不少。

另外,我也试过在OpenGL中使用影片纹理,不过我的方法使 CPU 占用率达 100%,但有一点可以肯定,这同样可应用于OpenGL。

敬告:如果您有兴趣读我的程序,请不要试图通过看我的 d3d 类来学习 Direct3D,那只是我前段时间为了学 D3D 而搭的框架类,有很多不规范的地方,而且 3D 物体更是一团糟,所写的代码是临时性的,根本没考虑可读性,现在我见到它们也很头痛,不想修整。所以为了不令您对 D3D 产生不好的印象,也为了保持您对 3D 世界探索的热情,请另行找资料系统学习 D3D。对于我的程序,您只要知道我在哪里使用了 d3d 类的哪些功能来实现效果就行了,切记!

程序的效率和其他问题

  1. 性能

在DS 封装类中我写了几个GDI画图函数,可以比较方便的显示图片,不过效率低下,我曾发现它的效能竟比D3D画 3D场景还差。这是GDI 的问题,大概它没有很好利用显卡而依赖CPU的缘故吧。所以我在使用 D3D 的时候顺便用上了 ID3DXSprite 接口,它是在没有 DirectDraw的情况下很好的2D 画图工具,而应用了DX 的特性使它效率很高,充分使用显卡加速,很容易做到旋转、缩放、透明等功能,效率很高。不过应注意一个问题:通常看到的D3D 初始化程序都是为得到最大的3D 效能,即使没干什么CPU占用率也会直上到100%,而播放视频文件一般都需要解码,CPU 都让D3D 占去后就没法流畅解码视频,所以应该这样创建D3D 设备,完成初始化:
 

m_pD3D->CreateDevice(D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,
hWnd,D3DCREATE_SOFTWARE_VERTEXPROCESSING|D3DCREATE_MULTITHREADED,&d3dpp,&m_pd3dDevice)

一定要加上一个D3DCREATE_MULTITHREADED,让D3D 解箍 CPU,D3D不忙的时候降至 0%,当然了,3D 效能会减掉一些,不过效果尚可。

还有一个,我发现程序运行需要占很多内存,但最小化后再打开就只占原来的一半左右,十分想不通。

2.程序错误的修正

为了程序正确运行,还要解决另一件事。您还记得回调函数吗?就是它惹起了祸。回调函数带来了灵活性的同时也带来了灾难。到目前为止我发现了两点。一是不 能在里面调用DirectShow 对象,例如在里面调用 IMediaPosition 来设置进度条,这样当按下“停止”时会发现 IMediaControl 的Stop 方法不能返回,我认为是递归调用所引起的,因此绝对不能这样做,无法在Filter内采取任何措施防止这种情况。

第二点直接体现灵活 性弱点。想想我需要在回调函数中干些什么,我需要调用函数设置D3D纹理吧,那我就要处理纹理在Filter还运作过程中发生参数改变或被释放的问题,在 播放过程中打开新文件或关闭程序都有可能发生这种情况。打开新文件时我们会先释放旧的Filter Graph再重建,释放并创建新的纹理会在启动播放后才进行,因为要得到视频参数,所以有可能Filter已经运作了而使用的纹理还是旧的,引起访问错 误。DirectShow 的Filter Chain 有自己的线程,调用IMediaControl 的Stop 并不能让它一下子停下来,突然关闭程序就有可能使纹理释放比真正结束播放早,因为我们不会在回调函数中管理纹理而在D3D类中进行管理,关闭时纹理会随 D3D类的释放而释放。

我刚解决问题的办法很稀奇古怪,也许任何一个大虾都反对,不过它工作的很好。定义一个BOOL变量,在打开文 件时先把它设为FALSE,结束打开时再设为TRUE,并重载对话框的OnDestroy函数,在该函数中把BOOL变量设为FALSE,这样在回调函数 开始处检查此变量,为FALSE时直接返回。后来发现把回调函数设为NULL也可,而且似乎更好。

3.与其他实用程序相比

其实微软为DirectShow新配备的VMR9可以无缝与D3D结合,更有很多强大的功能,绝非我写的Filter可比,不过我懒于学习使用它,自认 为写Filter比学习它更容易。从VMR9可以自动提供纹理给D3D使用,我萌生一个想法:能不能在一个D3D中创建纹理给另一个D3D 使用呢?我要进行验证。

最近得到一个叫做“CamTrack”的软件,它很厉害,自带的人脸跟踪功能自不必说,它还能自动侦测到摄像 头的使用,在比Filter更低的层级就接收并处理了数据,并把处理后的数据作为摄像头数据传送下来!我惊呆了,好像要从摄像头的WDM 驱动模型去找突破口,但完全没有头绪。

4.界面

除了技术落后、结构混杂之外,我的程序还有很大的一个缺陷:界面丑陋,元素排列不合理。这是因为我不熟悉MFC,也不懂得如何设计界面造成的,所以您看到的我的程序是古板的 win98 样式,用起来可能很不就手。我初三暑假开始接触编程,一直用了3年多VB6,鄙视VC,转到VC编程还是近大半年的事,虽已乐不知返却从没想过要努力学 MFC,毕竟VC.net的net界面编写已不再依赖MFC了,它更像VB,更易用,只是程序载入到界面显示很慢,我用的VC.net2003 是这样。

总结

在这次编写中我的体会良多,记得看过这样一句话“没写过一万行代码的项目是不会懂得 软件工程的”, 此时真的把它奉为真理了,想我这个程序才多大点儿,文件已有七八个,平时我看文件有几个的程序都感到头疼,更不敢想象上万行程序的管理,可能管理上花的精 力比编码更多。在标准化工厂里,工人都被分工,他们可能不知道自己在干什么,只有管理人员才对一切了如指掌。如果生产的是软件,单会编码的程序员就只能做 埋头的工人了,更高层次由不编码的管理人员掌握,事实也是这样。我还发现有时花在界面的时间比所谓的核心功能还多,正如为了控制发动机和轮子,汽车在控制 方面要花费很多,而在舒适的驾驶环境和新潮的外型方面更要努力,因为必须要为人所用、易用、好用。现在回过头看所写的Filter,它的确是各种功能的基 础,仅此而已,编码的比重并不大。总的说,我的感想是:现代软件开发应用的是现代工业生产模式,标准化、分工合作、流水线操作。当然,我的认识还很肤浅, 请指正。 最让我苦恼的是 DirectShow 不支持 rm 文件,或者说 RealNetworks 不支持 DirectShow,毕竟 rm 的算法是保密的,这导致无法用我的程序欣赏很多美妙的影片,强烈希望大虾们用real sdk 写个 Filter 发布出来,让我等享用,我找不着real sdk。

总算劳累后也有成果,总体效果没有受技术限制,再请看看几幅效果图,以结全文。程序路上,你我共勉。

 
 
 

你可能感兴趣的:(汇编,filter,dll,byte,templates,图像处理)