DirectShow Filter的開發實踐


一、介紹

     攝像頭圖像采集處理在業界有着多種成熟的方案。從老的DirectShow、Grabber技術,到新的Windows Media Foundation框架,網絡上都有着豐富的參考資料。OpenCV庫里面甚至提供了非常簡潔的接口,用戶只要一兩行代碼即可實現數據采集、編解碼等功能,使用起來甚是方便。但是,如果把數據采集的任務放到我們自己的程序中來實現的話,CPU的占用率會比較高。這在某些情況下不太可取。雖然可以實現,但是在客戶端使用時效率非常低下。而公司恰好有一項開發任務:要求采集到攝像頭數據后,對圖像數據進行各種變換處理,然后傳遞給底層驅動程序,實現虛擬攝像頭功能。具體的效果如果CamMask或者CamTwist:

 

     嘗試過自己寫代碼采集攝像頭數據,然后再進行圖像處理。但是換了多種方式都不太理想。要么CPU占用率達到百分之五六十,要么內存占用率達到六七百兆。采用DirectShow Filter似乎就成了唯一的一種方式。實際測試下來,3K分辨率的視頻CPU占用率保持在30%上下,內存在150M上下。這個數據還是可以接受的。

二、DirectShow基礎

    DirectShow是Microsoft DirectX技術體系中的一員,其他成員還包括DirectSound, DirectInput, DirectSetup, DirectX Graphics等。DirectShow技術是微軟為了解決多媒體應用開發中的一些難題而提出的。例如:如何保證數據量巨大的多媒體數據處理的高效性?如何讓音視頻時刻保持同步?如何處理各種式樣的媒體格式問題?如何支持目標系統中不可預知的硬件?DirectShow的設計初衷就是盡量讓應用程序開發人員從復雜的數據傳輸、硬件差異、同步性等工作中解脫出來,總體應用框架和底層工作由DirectShow來完成。DirectShow技術的總體運行流程如下:

     Filter是DirectShow技術體系中最基本的概念。如上圖所示,DirectShow中的Filter分成三大類:Source Filter、Transform Filter、Render Filter。Source Filter就是提供數據源的Filter,所有的數據都是從Source Filter流出去的。不管是多媒體文件還是多媒體設備,Source Filter都進行了封裝統一了接口,在使用方式上保持了一致。Transform Filter則是對數據進行操作處理的Filter,所有的圖像操作都應該在這里進行。而Render Filter則是用來渲染圖像的Filter,不管是保存到文件還是輸出到其他地方,都由這個Render Filter來實現。Windows系統本身提供了非常多的Filter,我們在開發的時候可以直接使用。

    

     DirectShow使用Filter Graph來管理Filter。Filter Graph是Filter的容器,所有Filter如果想要起作用,就必須加入到Filter Graph當中。Filter是Filter Graph當中最小的功能模塊。

     Filter加入到Filter Graph中后,還需要進行連接。Filter的連接,實際上就是Filter上的Pin的連接。連接的方式一般總是由上一級Filter的輸出Pin指向下一級Filter的輸入Pin。如下圖:

     圖中總共出現了5個Filter。其中MJPEG DecompressorColor Space Converter是系統提供的Filter,分別用於MJPG流的解碼和顏色空間轉換。Video Control則是GraphStudio自動綁定到相機的Filter,剩下的兩個是我們自己編寫的Filter,分別屬於Transform Filter和Render Filter。Filter之間的綠線就是表示Filter上Pin之間的連接。這樣連接來之后,整條Filter Graph就跑通了。這個工具名叫GraphStudioNext,用於測試Filter的編寫是否正確。更多的DirectShow基礎介紹,可以參考《DirectShow開發指南:陸其明著》這本書。正如其宣傳所言:全面深刻通俗易懂

三、編寫DirectShow Filter

     那么,代碼中如何編寫Filter呢?我們需要參考例子。網絡上及上面介紹的那本書中,都提到了DirectShow Samples這個玩意兒。但是我把Windows 10系統的SDK目錄翻了個底朝天也沒發現Samples在哪。后來經過研究才發現,貌似Windows 7的SDK中才附帶了Samples。也就是說,想要參考Samples里面的樣例工程,還得安裝個Windows 7的SDK。

     Filters目錄下面就是一些簡單的樣例工程。這其中要介紹一下的是baseclasses目錄。這下面的是一些C++類文件,是微軟實現的對DirectShow Filter API的封裝。這些類替我們把一些通用操作給抽象出來實現了,然后我們在實現自己的Filter時,直接從baseclasses里面的類繼承就好了,簡潔方便。如果不用Baseclasses里面的類的話,也可以進行DirectShow Filter的開發,但是需要自己實現很多重復、繁雜的代碼,還容易出錯。baseclasses里面有一個vs工程,需要我們用vs將baseclasses編譯成靜態庫,使用時包括頭文件即可。

     Filter的編寫在samples里面提供的工程基礎修改即可。關鍵的關鍵是Filter之間的連接,Filter上的Pin之間需要協商好,否則的話無法順利建立連接。協商過程主要是包括媒體類型、尺寸、顏色模型、壓縮方式等。這個過程需要在實際開發中去研究嘗試。Filter編寫好之后的工作,就剩下連接了。連接操作非常通俗簡單,簡而言之就是:實例化Filter->創建Filter Graph->往Filter Graph中加入Filter->查找Filter上的Pin->連接Pin->運行Filter Graph

// instantialize filters
IBaseFilterPtr	      m_pVCamRenderer;
IBaseFilterPtr          m_pInsta360TestFilter; 
IBaseFilterPtr          m_pJpegDecoder;
IBaseFilterPtr          m_pColorConverter;
HRESULT hr = CoCreateInstance(CLSID_VCamRenderer, NULL, CLSCTX_INPROC, IID_IBaseFilter, 
		reinterpret_cast<void**>(&m_pVCamRenderer));
if (FAILED(hr)) {
	LOGERR("Failed to create VCam Renderer filter!"); 
	m_initialStatus = FALSE;
}

hr |= CoCreateInstance(CLSID_Insta360TestSticher, NULL, CLSCTX_INPROC, IID_IBaseFilter,
	reinterpret_cast<void**>(&m_pInsta360TestFilter));
if (FAILED(hr)) { 
	LOGERR("Failed to create Insta360 test sticher filter!");
	m_initialStatus = FALSE;
} 

hr |= CoCreateInstance(CLSID_Colour, NULL, CLSCTX_INPROC, IID_IBaseFilter,
	reinterpret_cast<void**>(&m_pColorConverter));
if (FAILED(hr)) {
	LOGERR("Failed to create color space converter filter!");
	m_initialStatus = FALSE;
}

hr |= CoCreateInstance(CLSID_MjpegDec, NULL, CLSCTX_INPROC, IID_IBaseFilter,
	reinterpret_cast<void**>(&m_pJpegDecoder));
if (FAILED(hr)) {
	LOGERR("Failed to create jpeg decoder filter!");
	m_initialStatus = FALSE;
} 

// declare variables 
HRESULT hr;
IGraphBuilderPtr		m_pGraph;
IMediaSeekingPtr		m_pMS;
IMediaControlPtr		m_pMC;

CComPtr<IPin> m_pCameraOutput;
	
CComPtr<IPin> m_pDecoderInput;
CComPtr<IPin> m_pDecoderOutput;

CComPtr<IPin> m_pSticherInput;
CComPtr<IPin> m_pSticherOutput;

CComPtr<IPin> m_pColorConverterInput;
CComPtr<IPin> m_pColorConverterOutput;

CComPtr<IPin> m_pVCamInput;

// query interfaces
hr = CoCreateInstance(CLSID_FilterGraph, NULL,  CLSCTX_INPROC, IID_IGraphBuilder, (void **)&m_pGraph);
// Media control
hr = m_pGraph->QueryInterface(IID_IMediaControl, (void**)&m_pMC); 

hr = m_pGraph->AddFilter(pCapSrcFilter, L"Source");
hr = m_pGraph->AddFilter(m_pJpegDecoder, L"Decoder");
hr = m_pGraph->AddFilter(m_pInsta360TestFilter, L"Sticher");  
hr = m_pGraph->AddFilter(m_pColorConverter, L"Converter");
hr = m_pGraph->AddFilter(m_pVCamRenderer, L"Renderer");

// Enumerate pins to make connections
CComPtr<IEnumPins> pEnum = NULL;
hr = pCapSrcFilter->EnumPins(&pEnum);
hr = pEnum->Reset();
m_pCameraOutput = NULL;
hr = pEnum->Next(1, &m_pCameraOutput, NULL);
 
// decoder
pEnum = NULL;
m_pJpegDecoder->EnumPins(&pEnum);
pEnum->Reset();
m_pDecoderInput = NULL;
hr = pEnum->Next(1, &m_pDecoderInput, NULL); 

pEnum = NULL;
m_pJpegDecoder->EnumPins(&pEnum);
pEnum->Reset();
pEnum->Skip(1); 
m_pDecoderOutput = NULL;
hr = pEnum->Next(1, &m_pDecoderOutput, NULL); 

// sticher
pEnum = NULL;
m_pInsta360TestFilter->EnumPins(&pEnum);
pEnum->Reset();
m_pSticherInput = NULL;
hr = pEnum->Next(1, &m_pSticherInput, NULL); 

pEnum = NULL;
m_pInsta360TestFilter->EnumPins(&pEnum);
pEnum->Reset();
pEnum->Skip(1);
m_pSticherOutput = NULL;
hr = pEnum->Next(1, &m_pSticherOutput, NULL); 

// color space converter
pEnum = NULL;
m_pColorConverter->EnumPins(&pEnum);
pEnum->Reset();
m_pColorConverterInput = NULL;
hr = pEnum->Next(1, &m_pColorConverterInput, NULL);

pEnum = NULL;
m_pColorConverter->EnumPins(&pEnum);
pEnum->Reset();
pEnum->Skip(1);
m_pColorConverterOutput = NULL;
hr = pEnum->Next(1, &m_pColorConverterOutput, NULL);

// vcam
pEnum = NULL;
m_pVCamRenderer->EnumPins(&pEnum);
pEnum->Reset(); 
m_pVCamInput = NULL;
hr = pEnum->Next(1, &m_pVCamInput, NULL);
if (FAILED(hr))
{
	LOGERR("Error occured while enumerating filter pins.");
	SAFE_RELEASE(pCapSrcFilter);
	SAFE_RELEASE(pM);
	return hr;
}

hr |= m_pGraph->Connect(m_pCameraOutput, m_pDecoderInput);
if (FAILED(hr))
{
	LOGERR("Failed to connect filter pins: 0x%X", hr);
	SAFE_RELEASE(pCapSrcFilter);
	SAFE_RELEASE(pM);
	return hr;
}
hr |= m_pGraph->Connect(m_pDecoderOutput, m_pSticherInput); 
if (FAILED(hr))
{
	LOGERR("Failed to connect filter pins: 0x%X", hr);
	SAFE_RELEASE(pCapSrcFilter);
	SAFE_RELEASE(pM);
	return hr;
}
hr |= m_pGraph->Connect(m_pSticherOutput, m_pColorConverterInput);
if (FAILED(hr))
{
	LOGERR("Failed to connect filter pins: 0x%X", hr);
	SAFE_RELEASE(pCapSrcFilter);
	SAFE_RELEASE(pM);
	return hr;
}
hr |= m_pGraph->Connect(m_pColorConverterOutput, m_pVCamInput);
if (FAILED(hr))
{
	LOGERR("Failed to connect filter pins: 0x%X", hr);
	SAFE_RELEASE(pCapSrcFilter);
	SAFE_RELEASE(pM);
	return hr;
}

// run the graph 
if (NULL != m_pMC)
{
	m_pMC->Run();
} 

SAFE_RELEASE(pCapSrcFilter);
SAFE_RELEASE(pM); 

  至此,整條Filter Graph就跑起來了。我們的DirectShow應用程序也就編寫完成了。實際編寫過程中可能會遇到更多的問題,此時多嘗試多搜索,通常都可以解決掉。在本人編寫Transform Filter的過程中,有一個需要改變輸出尺寸的需求。也就是說,輸入的是2:1的視頻的話,我要改成1:1的輸出。這里要一定要注意CTransformFilter類的CheckTransform方法。一般如果輸入輸出的媒體類型不變的話,實現如下:

// Check a transform can be done between these formats
HRESULT Insta360TestFilter::CheckTransform(const CMediaType* mtIn, const CMediaType* mtOut)
{  
	CheckPointer(mtIn, E_POINTER);
	CheckPointer(mtOut, E_POINTER);

	if (CanPerformTransform(mtIn))
	{
		if (*mtIn == *mtOut)
		{
			return NOERROR;
		}
	}

	return E_FAIL;
}

  但是如果Filter的輸入輸出媒體類型發生改變了的話,這里就不能這么寫了。要么在這里對mtIn和mtOut進行修改保證相等,要么直接返回NOERROR。否則編寫出來的Filter是無法和其他Filter進行連接的!

四、參考鏈接

五、模板工程

      Transform Filter模板工程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM