Skip to content

DirectShowによるカメラキャプチャ

odahiro edited this page Dec 21, 2018 · 1 revision

Windows上ではDirectShowを利用することによって比較的簡単にカメラデバイスからのキャプチャを行うことができます。 ここでは、そのDirectShowを用いてカメラキャプチャを行い、キャプチャした画像をMISTのコンテナに格納する方法を説明します。

DirectShowとは

DirectShowとはMicrosoft社のWindows用マルチメディアAPIのことです。 これを利用することによって今回の目的であるカメラデバイスからのキャプチャのほかにも動画や音声などのストリーミング再生などにも利用することができます。 ただしWindows上のみで作動するため、Linux上などでは利用できません。

DirectShow使用のための設定

DirectShowによるプログラムを利用するためには、以下のページからダウンロードできるPlatform SDKが必要になります。 ​ http://www.microsoft.com/downloads/details.aspx?FamilyId=A55B6B43-E24F-4EA3-A93E-40C0EC4F68E5&displaylang=en

またビルドするためには、「dshow.h」「qedit.h」をインクルード、「Strmiids.lib」をリンクする必要があります。

処理の流れ

最下部に全体のソースコードを記載しています。 以下は処理の流れの概要を説明します。

1. 初期化

!DirectShowはCOMであるため、最初にCoInitialize関数を呼び出す必要があります。

// 初期化
CoInitialize(0);

2. キャプチャデバイスの列挙

キャプチャするためにはキャプチャデバイスが存在しないといけないため、現在PCに接続されているデバイスを列挙しています。 今回は、見つかった場合には一番目のデバイスを使用するようにしています。

// キャプチャデバイスを列挙する
ICreateDevEnum *deviceEnum = 0;
CoCreateInstance(CLSID_SystemDeviceEnum,0,CLSCTX_INPROC,IID_ICreateDevEnum,reinterpret_cast<void**>(&deviceEnum));

IEnumMoniker *classEnum;
deviceEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&classEnum,0);
if(classEnum == 0)
{
    std::cout << "キャプチャデバイスは存在しません" << std::endl;
    safe_release(deviceEnum);
    exit(1);
}
safe_release(deviceEnum);

// 最初のモニカをフィルタにバインドする
IMoniker *moniker = 0;
ULONG fetched;
if(classEnum->Next(1,&moniker,&fetched) == S_OK)
{
    moniker->BindToObject(0,0,IID_IBaseFilter,reinterpret_cast<void**>(&baseFilter));
    safe_release(moniker);
}
safe_release(classEnum);

3. フィルタの構築

DirectShowは、フィルタを接続することによってデータの流れを制御しているため、キャプチャ用のフィルタを構築します。 また、キャプチャ用のフィルタを作成する際に取得するデータ形式を指定しますが、今回はRGB24bitで取得するように設定しています。 (AM_MEDIA_TYPEの設定を変更することで取得するデータ形式を変更できます。)

// フィルタの構築
CoCreateInstance(CLSID_FilterGraph,0,CLSCTX_INPROC,IID_IGraphBuilder,reinterpret_cast<void**>(&graph));
graph->QueryInterface(IID_IMediaControl,reinterpret_cast<void**>(&media));
graph->AddFilter(baseFilter,L"Capture");

CoCreateInstance(CLSID_SampleGrabber,0,CLSCTX_INPROC_SERVER,IID_IBaseFilter,reinterpret_cast<void**>(&filter));
filter->QueryInterface(IID_ISampleGrabber,reinterpret_cast<void**>(&grabber));

// 取得するデータ形式を設定
AM_MEDIA_TYPE mt;
memset(&mt,0,sizeof(mt));
mt.majortype = MEDIATYPE_Video;
mt.subtype = MEDIASUBTYPE_RGB24;
mt.formattype = FORMAT_VideoInfo;
grabber->SetMediaType(&mt);

// サンプルグラバを追加
graph->AddFilter( filter, L"Grabber" );

// ピンを接続する
IPin *pSrcO     = get_pin( baseFilter, PINDIR_OUTPUT ); // 入力ピン
IPin *pGrabberI = get_pin( filter,     PINDIR_INPUT  ); // 出力ピン
IPin *pGrabberO = get_pin( filter,     PINDIR_OUTPUT  );    // 出力ピン
graph->Connect( pSrcO, pGrabberI );
graph->Render( pGrabberO );
safe_release( pSrcO );
safe_release( pGrabberI );
safe_release( pGrabberO );

CoCreateInstance(CLSID_CaptureGraphBuilder2,0,CLSCTX_INPROC,IID_ICaptureGraphBuilder2,reinterpret_cast<void**>(&builder));
builder->SetFiltergraph(graph);
builder->RenderStream(&PIN_CATEGORY_PREVIEW,&MEDIATYPE_Video,baseFilter,0,filter);
safe_release(baseFilter);

4. 取得する画像に応じたMISTコンテナの設定

キャプチャしたデータを格納するMISTコンテナの大きさを設定します。 (キャプチャされる画像データはAM_MEDIA_TYPE::pbFormatをVIDEOINFOHEADERにキャストすることで取得できます。)

// 取得先の画像情報を設定
grabber->GetConnectedMediaType(&mt);
VIDEOINFOHEADER *videoHeader = reinterpret_cast<VIDEOINFOHEADER*>(mt.pbFormat);
image.resize(videoHeader->bmiHeader.biWidth,videoHeader->bmiHeader.biHeight);

5. キャプチャ開始

実際にキャプチャ処理を開始します。 DirectShowにはコールバックによってキャプチャを行う方法も用意されていますが、今回はスレッドによってキャプチャを行っています。 ISampleGrabber::SetBufferSamples?(TRUE)、IMediaControl::Runを呼び出すことでキャプチャが可能な状態になります。

// キャプチャ開始
grabber->SetBufferSamples(TRUE);
media->Run();

// キャプチャスレッドを開始する
captured = true;
HANDLE hThread  = reinterpret_cast<HANDLE>(_beginthreadex(0,0,&captureThread,0,0,0));

// キーが押されるまでキャプチャを続ける
getchar();
captured = false;
WaitForSingleObject(hThread,INFINITE);
CloseHandle( hThread );

6. キャプチャスレッド

実際にキャプチャ処理を行っているスレッドの処理です。 ISampleGrabber::GetCurrentBufferを呼び出すことでカメラからキャプチャ画像を取得することができます。 取得したキャプチャ画像はBGR順に並び、上下が反転しています。 今回はRGB順に並び、上下が反転していないものへと変換してMISTコンテナへ格納しています。

// キャプチャスレッド
unsigned int __stdcall captureThread(void *p)
{
    // 一時バッファを作成
    unsigned char *buf = new unsigned char[image.width()*image.height()*3];

    // キャプチャが行われている限りスレッドを続ける
    while(captured)
    {
        // キャプチャしたデータは上下が逆、BGR順なので一時バッファに退避してひっくり返す
        long size = static_cast<long>(image.width()*image.height()*3);
        grabber->GetCurrentBuffer(&size,reinterpret_cast<long*>(buf));

        unsigned char *p = buf;
        for(int y=0;y<static_cast<int>(image.height());++y)
        {
            for(int x=0;x<static_cast<int>(image.width());++x)
            {
                image(x,image.height()-1-y).r = *(p+2);
                image(x,image.height()-1-y).g = *(p+1);
                image(x,image.height()-1-y).b = *p;

                p += 3;
            }
        }

        //
        // 任意の画像処理を行う
        //

        // キャプチャした画像をファイルに保存する
        mist::write_bmp(image,"test.bmp");
    }

    // 一時バッファを破棄
    delete [] buf;

    return 0;
}

全体のソースコード

以上の処理に必要な変数や関数を加えて、以下のように実装します。

#include <mist/mist.h>
#include <mist/io/bmp.h>
#include <dshow.h>
#include <qedit.h>
#include <process.h>


// インターフェイスを安全に解放する

template < class T >
void safe_release( T *&p )
{
    if( p )
    {
        p->Release( );
        p = 0;
    }
}

// フィルターの指定した方向のピンを取得します。
IPin *get_pin( IBaseFilter *pFilter, PIN_DIRECTION PinDir )
{
    BOOL bFound = FALSE;
    IEnumPins *pEnum;
    IPin *pPin;

    pFilter->EnumPins( &pEnum );

    while( pEnum->Next( 1, &pPin, 0 ) == S_OK )
    {
        PIN_DIRECTION PinDirThis;
        pPin->QueryDirection( &PinDirThis );

        if( bFound = ( PinDir == PinDirThis ) )
        {
            break;
        }

        pPin->Release( );
    }

    pEnum->Release( );

    return( bFound ? pPin : 0 );
}


// 出力を行う画像コンテナ
mist::array2< mist::rgb< unsigned char > > image;

// キャプチャ用インターフェイス
ISampleGrabber *grabber = 0;

// キャプチャフラグ
bool captured;

// キャプチャスレッド
unsigned int __stdcall captureThread( void *p )
{
    // 一時バッファを作成
    unsigned char * buf = new unsigned char[ image.width( ) * image.height( ) * 3 ];

    // キャプチャが行われている限りスレッドを続ける

    while( captured )
    {
        // キャプチャしたデータは上下が逆、BGR順なので一時バッファに退避してひっくり返す
        long size = static_cast< long >( image.width( ) * image.height( ) * 3 );
        grabber->GetCurrentBuffer( &size, reinterpret_cast< long * >( buf ) );

        unsigned char *p = buf;

        for( int y = 0 ; y < static_cast < int >( image.height( ) ) ; ++y )
        {
            for( int x = 0 ; x < static_cast < int >( image.width( ) ) ; ++x )
            {
                image( x, image.height( ) - 1 - y ).r = *( p + 2 );
                image( x, image.height( ) - 1 - y ).g = *( p + 1 );
                image( x, image.height( ) - 1 - y ).b = *p;

                p += 3;
            }
        }

        // キャプチャした画像をファイルに保存する
        mist::write_bmp( image, "test.bmp" );
    }

    // 一時バッファを破棄
    delete[] buf;

    return( 0 );
}

// メイン関数
int main( int argc, char *argv[] )
{
    // キャプチャ用インターフェイス
    IBaseFilter * baseFilter = 0;
    IGraphBuilder *graph = 0;
    ICaptureGraphBuilder2 *builder = 0;
    IMediaControl *media = 0;
    IBaseFilter *filter = 0;

    // 初期化
    CoInitialize( 0 );

    // キャプチャデバイスを列挙する
    ICreateDevEnum *deviceEnum = 0;
    CoCreateInstance( CLSID_SystemDeviceEnum, 0, CLSCTX_INPROC, IID_ICreateDevEnum, reinterpret_cast< void * * >( &deviceEnum ) );

    IEnumMoniker *classEnum;
    deviceEnum->CreateClassEnumerator( CLSID_VideoInputDeviceCategory, &classEnum, 0 );

    if( classEnum == 0 )
    {
        std::cout << "キャプチャデバイスは存在しません" << std::endl;
        safe_release( deviceEnum );
        exit( 1 );
    }

    safe_release( deviceEnum );

    // 最初のモニカをフィルタにバインドする
    IMoniker *moniker = 0;
    ULONG fetched;

    if( classEnum->Next( 1, &moniker, &fetched ) == S_OK )
    {
        moniker->BindToObject( 0, 0, IID_IBaseFilter, reinterpret_cast< void * * >( &baseFilter ) );
        safe_release( moniker );
    }

    safe_release( classEnum );

    // フィルタの構築
    CoCreateInstance( CLSID_FilterGraph, 0, CLSCTX_INPROC, IID_IGraphBuilder, reinterpret_cast< void * * >( &graph ) );

    graph->QueryInterface( IID_IMediaControl, reinterpret_cast< void * * >( &media ) );
    graph->AddFilter( baseFilter, L"Capture" );

    CoCreateInstance( CLSID_SampleGrabber, 0, CLSCTX_INPROC_SERVER, IID_IBaseFilter, reinterpret_cast< void * * >( &filter ) );
    filter->QueryInterface( IID_ISampleGrabber, reinterpret_cast< void * * >( &grabber ) );

    // 取得するデータ形式を設定
    AM_MEDIA_TYPE mt;
    memset( &mt, 0, sizeof( mt ) );
    mt.majortype = MEDIATYPE_Video;
    mt.subtype = MEDIASUBTYPE_RGB24;
    mt.formattype = FORMAT_VideoInfo;
    grabber->SetMediaType( &mt );

    // サンプルグラバを追加
    graph->AddFilter( filter, L"Grabber" );

    // ピンを接続する
    IPin *pSrcO     = get_pin( baseFilter, PINDIR_OUTPUT ); // 入力ピン
    IPin *pGrabberI = get_pin( filter,     PINDIR_INPUT  ); // 出力ピン
    IPin *pGrabberO = get_pin( filter,     PINDIR_OUTPUT  );    // 出力ピン
    graph->Connect( pSrcO, pGrabberI );
    graph->Render( pGrabberO );
    safe_release( pSrcO );
    safe_release( pGrabberI );
    safe_release( pGrabberO );

    CoCreateInstance( CLSID_CaptureGraphBuilder2, 0, CLSCTX_INPROC, IID_ICaptureGraphBuilder2, reinterpret_cast< void * * >( &builder ) );
    builder->SetFiltergraph( graph );
    builder->RenderStream( &PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, baseFilter, NULL, NULL );
    safe_release( baseFilter );

    // 取得先の画像情報を設定
    grabber->GetConnectedMediaType( &mt );
    VIDEOINFOHEADER *videoHeader = reinterpret_cast< VIDEOINFOHEADER * >( mt.pbFormat );
    image.resize( videoHeader->bmiHeader.biWidth, videoHeader->bmiHeader.biHeight );

    // キャプチャ開始
    grabber->SetBufferSamples( TRUE );
    media->Run( );

    // キャプチャスレッドを開始する
    captured = true;
    HANDLE hThread = reinterpret_cast< HANDLE >( _beginthreadex( 0, 0, &captureThread, 0, 0, 0 ) );

    // キーが押されるまでキャプチャを続ける
    getchar( );
    captured = false;
    WaitForSingleObject( hThread, INFINITE );
    CloseHandle( hThread );

    // インターフェイスをリリースする
    safe_release( builder );
    safe_release( media );
    safe_release( filter );
    safe_release( grabber );

    return( 0 );
}