极简主义方法实现实时视频图像处理/帧捕获






4.40/5 (138投票s)
可能是Windows上最简单的视频图像捕获示例。支持AVI、MPG、QTV、摄像头、电视调谐器等。
引言
我一生都痴迷于一切与机器人和计算机视觉相关的事物。所以最近,我一直在寻找一些简单的代码来读取各种格式的电影和摄像头的图像。在这里,我想与你分享我在这个API丛林中的经验。另外,请考虑使用下面的投票条给我投票,以便我了解这些信息是否对任何人有用。
背景
尽管我费尽心思寻找,但我发现的都是VFW接口的示例,这是一个几乎已经死亡的API,有很多问题。首先,它只支持AVI文件,而且大多数编解码器(如divx)在其中根本无法工作(试过的都知道),而且在某些分辨率下,在大多数摄像头上,它就会崩溃。有一些关于较新的DirectShow API的示例,但所有重要的代码都埋藏在数以千计不重要的包装器/支持类/文件中。更不用说示例本身的信息价值经常在冗余、错误处理的丛林中丢失。最后,它们是基于使用/编译额外的MS DirectShow过滤器示例,如SampleGrabber或其bug修复版本GrabberSample,这使得整个方法在SDK版本之间不具有可移植性。它们似乎也从最新的SDK和网络上消失了。所以,这是我认为一个更简单/更通用的替代方案。
无需DirectShow SDK的新方法
最近,我将此新方法添加到文章中,因为很多人在编译DirectShow SDK相关内容时遇到了问题。他们中的大多数人只需要获取RGB数据,而忽略整个DirectShow的东西,所以我产生了一个想法。如果我们知道数据在DirectShow图表中是如何流动的,为什么不直接重定向特定方法来捕获数据呢?
在DirectShow中,数据可以通过以下方式流动:
推送模式: FILTER1->输出引脚通过其IMemInputPin接口上的Receive将数据推送到FILTER2的输入引脚(我们的Renderer情况)。
拉取模式: 第二个过滤器FILTER2通过其IAsyncReader接口上的SyncReadAlligned方法,向第一个源FILTER1->out请求更多数据。
在我们的例子中(最后一个过滤器Renderer),我们捕获的是推送模式的数据。你可以在我的另一篇文章Fun with Google TTS中找到捕获拉取模式的示例。
我在Visual Studio 2008和2010上测试了以下代码,它在没有DirectShow SDK的情况下也能很好地编译。另外请记住,如果你需要RGB数据,你可能需要转换接收到的数据,但这很容易。
代码
#include <windows.h> #include <dshow.h> #pragma comment(lib,"Strmiids.lib") #define DsHook(a,b,c) if (!c##_) { INT_PTR* p=b+*(INT_PTR**)a; VirtualProtect(&c##_,4,PAGE_EXECUTE_READWRITE,&no);\ *(INT_PTR*)&c##_=*p; VirtualProtect(p, 4,PAGE_EXECUTE_READWRITE,&no); *p=(INT_PTR)c; } // Here you get image video data in buf / len. Process it before calling Receive_ because renderer dealocates it. HRESULT ( __stdcall * Receive_ ) ( void* inst, IMediaSample *smp ) ; HRESULT __stdcall Receive ( void* inst, IMediaSample *smp ) { BYTE* buf; smp->GetPointer(&buf); DWORD len = smp->GetActualDataLength(); HRESULT ret = Receive_ ( inst, smp ); return ret; } int WINAPI WinMain(HINSTANCE inst,HINSTANCE prev,LPSTR cmd,int show){ HRESULT hr = CoInitialize(0); MSG msg={0}; DWORD no; IGraphBuilder* graph= 0; hr = CoCreateInstance( CLSID_FilterGraph, 0, CLSCTX_INPROC,IID_IGraphBuidder, (void **)&graph ); IMediaControl* ctrl = 0; hr = graph->QueryInterface( IID_IMediaControl, (void **)&ctrl ); ICreateDevEnum* devs = 0; hr = CoCreateInstance (CLSID_SystemDeviceEnum, 0, CLSCTX_INPROC, IID_ICreateDevEnum, (void **) &devs); IEnumMoniker* cams = 0; hr = devs?devs->CreateClassEnumerator (CLSID_VideoInputDeviceCategory, &cams, 0):0; IMoniker* mon = 0; hr = cams->Next (1,&mon,0); // get first found capture device (webcam?) IBaseFilter* cam = 0; hr = mon->BindToObject(0,0,IID_IBaseFilter, (void**)&cam); hr = graph->AddFilter(cam, L"Capture Source"); // add web cam to graph as source IEnumPins* pins = 0; hr = cam?cam->EnumPins(&pins):0; // we need output pin to autogenerate rest of the graph IPin* pin = 0; hr = pins?pins->Next(1,&pin, 0):0; // via graph->Render hr = graph->Render(pin); // graph builder now builds whole filter chain including MJPG decompression on some webcams IEnumFilters* fil = 0; hr = graph->EnumFilters(&fil); // from all newly added filters IBaseFilter* rnd = 0; hr = fil->Next(1,&rnd,0); // we find last one (renderer) hr = rnd->EnumPins(&pins); // because data we are intersted in are pumped to renderers input pin hr = pins->Next(1,&pin, 0); // via Receive member of IMemInputPin interface IMemInputPin* mem = 0; hr = pin->QueryInterface(IID_IMemInputPin,(void**)&mem); DsHook(mem,6,Receive); // so we redirect it to our own proc to grab image data hr = ctrl->Run(); while ( GetMessage( &msg, 0, 0, 0 ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } };
更改摄像头分辨率
您还可以选择在获取捕获设备输出引脚后,立即添加以下代码来设置分辨率。[感谢论坛中LightWing的提示!]
... IPin* pin = 0; hr = pins?pins->Next(1,&pin, 0):0; // via graph->Render IAMStreamConfig* cfg = 0; hr = pin->QueryInterface( IID_IAMStreamConfig, (void **)&cfg); // (Those are optional steps to set better resolution) int sz,max_res = 0; hr = cfg->GetNumberOfCapabilities(&max_res, &sz); VIDEO_STREAM_CONFIG_CAPS cap[2]; // find last = AM_MEDIA_TYPE* fmt = 0; hr = cfg->GetStreamCaps(max_res-1, &fmt, (BYTE*)cap); // max supported resolution (cap contains res x and y sizes) hr = cfg->SetFormat(fmt); // and set it to device before capture starts ...
需要DirectShow SDK的旧方法
从DirectShow开始
大多数用户第一次尝试从网上编译任何DirectShow示例时,会花费90%的时间去寻找SDK本身(因为现在它已经从DirectX SDK转移到了Platform SDK,所以请确保获取最新版本),然后花一天的时间来解决未定义变量的问题(将 *YOUR_PLATFORMSDK_DIR\Samples\Multimedia\DirectShow\BaseClasses* 添加到编译器的包含路径),以及未解析的外部符号(将 *strmiids.lib*、*winmm.lib* 和 *strmbasd.lib*(发布版本为*strmbase.lib*)添加到链接器的输入中)。但是后两个库不是Microsoft分发的,通常需要手动构建(只需运行*vcvars32*并在上述包含目录中运行*nmake*(发布库为*nmake nodebug=1*),然后将库从*nmake*创建的子目录中复制过来)。
使用代码
所以,这是示例。只需在你使用的任何开发环境中创建一个空的Windows C++应用程序(在VC++中禁用UNICODE,否则会出现链接错误),然后复制/粘贴到其中。我将其精简到最低限度以使其运行,因此请原谅它在不重要代码部分压缩的格式。一如既往,你可以通过在Google中搜索任何使用的API名称,并使用找到的第一个(通常是MSDN)页面来获取信息。
#include <windows.h> #include <dshow.h> #include <streams.h> int w,h; HWND hwnd; MSG msg = {0}; BITMAPINFOHEADER bmih={0}; struct __declspec( uuid("{71771540-2017-11cf-ae26-0020afd79767}") ) CLSID_Sampler; struct Sampler : public CBaseVideoRenderer { Sampler( IUnknown* unk, HRESULT *hr ) : CBaseVideoRenderer(__uuidof(CLSID_Sampler), NAME("Frame Sampler"), unk, hr) {}; HRESULT CheckMediaType(const CMediaType *media ) { VIDEOINFO* vi; if(!IsEqualGUID( *media->Subtype(), MEDIASUBTYPE_RGB24) || !(vi=(VIDEOINFO *)media->Format()) ) return E_FAIL; bmih=vi->bmiHeader; SetWindowPos(hwnd,0,0,0,20+(w=vi->bmiHeader.biWidth),60+(h=vi->bmiHeader.biHeight),SWP_NOZORDER|SWP_NOMOVE); return S_OK; } HRESULT DoRenderSample(IMediaSample *sample){ BYTE* data; sample->GetPointer( &data ); // Process RGB Frame data* here. For Example: ZeroMemory(data+w*h,w*h); BITMAPINFO bmi={0}; bmi.bmiHeader=bmih; RECT r; GetClientRect( hwnd, &r ); HDC dc=GetDC(hwnd); StretchDIBits(dc,0,16,r.right,r.bottom-16,0,0,w,h,data,&bmi,DIB_RGB_COLORS,SRCCOPY); ReleaseDC(dc); return S_OK; } HRESULT ShouldDrawSampleNow(IMediaSample *sample, REFERENCE_TIME *start, REFERENCE_TIME *stop) { return S_OK; // disable droping of frames } }; int WINAPI WinMain(HINSTANCE inst,HINSTANCE prev,LPSTR cmd,int show){ HRESULT hr = CoInitialize(0); hwnd=CreateWindowEx(0,"LISTBOX",0,WS_SIZEBOX,0,0,0,0,0,0,0,0); IGraphBuilder* graph= 0; hr = CoCreateInstance( CLSID_FilterGraph, 0, CLSCTX_INPROC,IID_IGraphBuilder, (void **)&graph ); IMediaControl* ctrl = 0; hr = graph->QueryInterface( IID_IMediaControl, (void **)&ctrl ); Sampler* sampler = new Sampler(0,&hr); IPin* rnd = 0; hr = sampler->FindPin(L"In", &rnd); hr = graph->AddFilter((IBaseFilter*)sampler, L"Sampler"); ICreateDevEnum* devs = 0; hr = CoCreateInstance (CLSID_SystemDeviceEnum, 0, CLSCTX_INPROC, IID_ICreateDevEnum, (void **) &devs); IEnumMoniker* cams = 0; hr = devs?devs->CreateClassEnumerator (CLSID_VideoInputDeviceCategory, &cams, 0):0; IMoniker* mon = 0; hr = cams?cams->Next (1, &mon, 0):0; IBaseFilter* cam = 0; hr = mon?mon->BindToObject(0,0,IID_IBaseFilter, (void**)&cam):0; IEnumPins* pins = 0; hr = cam?cam->EnumPins(&pins):0; IPin* cap = 0; hr = pins?pins->Next(1,&cap, 0):0; hr = graph->AddFilter(cam, L"Capture Source"); IBaseFilter* vid = 0; hr = graph->AddSourceFilter (L"c:\\Windows\\clock.avi", L"File Source", &vid); IPin* avi = 0; hr = vid?vid->FindPin(L"Output", &avi):0; hr = graph->Connect(cap?cap:avi,rnd); hr = graph->Render( cap?cap:avi ); hr = ctrl->Run(); SendMessage(hwnd, LB_ADDSTRING, 0, (long)(cap?"Capture source ...":"File source ...")); ShowWindow(hwnd,SW_SHOW); while ( msg.message != WM_QUIT) { if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); if(msg.message == WM_KEYDOWN && msg.wParam==VK_ESCAPE ) break; } Sleep(30); } };
关注点
最重要的几行在 DoRenderSample
中,因为在那里你可以在每一帧中以RGB格式获取图像。该示例本身会尝试查找并连接到第一个找到的视频捕获设备上的第一个找到的引脚。如果没有找到,它会尝试打开并运行指定路径中的视频文件。所以实际上,这是两个教程合二为一。一个很棒的附加功能是,你可以使用Alt+PrintScreen键来截取(任何?)电影/摄像头的屏幕截图,并将其粘贴/保存到你喜欢的图像编辑器中。
可增强之处
考虑使用某种智能指针,如 CComPtr<IGRAPHBUILDER>
,而不是原始指针,如 IGraphBuilder*
。我删除了它们,以确保没有ATL库的用户也能编译该示例。示例代码可以让你更好地控制视频播放(在这种情况下是自动倒带)。
... IMediaControl* ctrl = 0; hr = graph->QueryInterface( IID_IMediaControl, (void **)&ctrl ); IMediaSeeking* seek = 0; hr = graph->QueryInterface( IID_IMediaSeeking, (void **)&seek ); IMediaEvent* event = 0; hr = graph->QueryInterface( IID_IMediaEventEx, (void **)&event ); ... while ( msg.message != WM_QUIT) { long code, a, b; event->GetEvent(&code, &a, &b, 0); if( code == EC_COMPLETE ) { LONGLONG pos=0; hr=seek->SetPositions(&pos,AM_SEEKING_AbsolutePositioning,0,0); hr=ctrl->Run(); } ...
你可以添加此代码来在graphedit中(图形化地)检查(连接到远程图)整个渲染图(例如,用于调试捕获输入引脚)。
WCHAR wsz[256]; (void)StringCchPrintfW(wsz, NUMELMS(wsz),L"FilterGraph %08x pid %08x\0", (DWORD_PTR) 0, GetCurrentProcessId()); ... IGraphBuilder* graph = 0; hr = CoCreateInstance( CLSID_FilterGraph, 0, CLSCTX_INPROC,IID_IGraphBuilder, (void **)&graph ); IMoniker* moniker = 0; hr = CreateItemMoniker(L"!", wsz, &moniker); DWORD regno = 0xfedcba98; IRunningObjectTable* gedit = 0; hr = GetRunningObjectTable(0, &gedit); hr = gedit->Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, graph, moniker, ®no);
来自论坛用户的一些宝贵提示。
未解析的外部符号:确保你已构建文章中提到的库。
VS6:获取Platform SDK,不要忘记设置标准的SDK包含/库路径。
VS2005:未解析的外部符号
转到“配置属性” -> “常规” -> “字符集”
默认情况下,它设置为“使用Unicode字符集”。将其更改为“未设置”,项目应该可以链接。
请享用,并且不要忘记,大部分高级错误处理等都需要您自己实现,这只是一个非常基础的示例,以便您专注于它的工作原理。