DirectShowのビデオキャプチャプログラミング

Sony DFW-VL500(IEEE1394) + DirectShow(Windows) ・・・で,どうやって?

(注意)下記ドキュメントは2001年時点での情報です.例えば DirectShowは 2006年現在ではDirectXではなく Platform SDK ("Platform SDK" + "Web Install"などでウェブ全体からサーチ) に入っていたりします.

ん?まてまて.そもそも,VCすらほとんど使ったこと無いやん.
DirectX と ActiveX の区別がついたのがつい最近・・・香港映画との区別もつ かなかった三年前に比べれば,えらい進歩やね
じゃあなんでDirectShowをやるの?・・・そこにDirectShowがあるから
というわけで,あまり無理せんとできるだけシンプルに理解する.サンプルコー ドは main関数を用いたコンソールアプリケーションとし,1画面で収まることを 目指そう.


  1. DirectShowキャプチャプログラミングの準備
    1. Graph Edit で遊ぶ
    2. COM の浅めの理解
    3. DirectShowのHellow World
  2. DirectShow サンプルを眺める
    1. StillCap (Still, Video のキャプチャサンプル)
      C:\mssdk\samples\Multimedia\DirectShow\Editing\StillCap
    2. AMCap (Videoのキャプチャ,カメラのパラメタコントロール)
      C:\mssdk\samples\Multimedia\DirectShow\Capture\AMCap
  3. DirectShowでビデオキャプチャ
    ICaptureGraphBuilder2とIFileSinkFilter
    1. 基本的流れ
    2. デバイスの列挙
    3. ソースリスト(videocap_console)
  4. フィルタプロパティの取得
    1. フィルタの列挙
    2. フィルタのプロパティページ表示 ISpecifyPropertyPages
  5. DirectShowでスチルキャプチャ(その1: GetCurrentBuffer編)
    Streamから一枚取る ISampleGrabber + GetCurrentBuffer
    1. 基本的流れ
    2. ピンの列挙
    3. ソースリスト(stillcap_console1)
  6. DirectShowでスチルキャプチャ(その2:コールバック 編)
    Streamから一枚取る ISampleGrabber + ISampleGrabberCB
    1. 基本的流れ
    2. コールバックインタフェース の実装
    3. ソースリスト(stillcap_console2)
  7. カメラコントロール
    1. 基本的流れ
    2. ソースリスト(cameracontrol)
  8. 番外編
    1. 自分のフィルタグラフをGraphEditで確認
    2. コンソールウインドウを消す
  9. 複数カメラでのキャプチャ
    1. 基本的流れ
    2. ソースリスト(two_cameras)
    3. フィルタグラフ
  10. MDIなGUI(省略)

(注)ドキュメントの Help(...)は,DirectX 8.0 日本語へルプ をデフォルトのパス(C:\mssdk\doc\DX8JHelp\directx8_c.chm)にインストールし たものとして,ローカルディスクにリンクを張っている.

DirectShowキャプチャプログラミングの準備

Graph Editで遊ぶ

  1. スタート → プログラム → Microsoft DirectX 8 SDK → DirectX Utilities → Graph Edit を開く.
  2. Graph → Insert Filtersでフィルタを選ぶ
    例えば
    • Video Capture Sources
    • DirectShow Filter → Video Renderer
  3. フィルタのピンを接続する(マウスで入力ピンから出力ピンへドラッグ)
    必要なフィルタはある程度自動的に挿入される(Graph → Connect Intelligent のチェックを入れておく).
  4. 再生ボタンを押してグラフをプレイしてみる
詳しい遊び方
Help(DirectShow の使い方 → GraphEditによるグラフ構築のシミュレーション)

COMの浅めの理解

まぁ,要はインタフェースが重要らしい
dll入れ替えられるんやね
CoCreateInstanceでインスタンスつくって,インタフェースのポインタは QueryInterfaceで取得する
GUID(CLSID, IID)でインタフェースや実装クラスを識別する
こんなんで,ええか?
CComPtr ? ATL ? はあとまわし


DirectShowのHellow World

DirectXのへルプにあるDirectShowのファーストステップを参考に,まずは DirectShow のHellow World と呼ばれる サンプルコードを書いてみる.

  1. Win32コンソールアプリケーションとしてプロジェクトを作成
  2. プロジェクト→設定→リンクに,strmiids.libを追加する.
  3. dshow.hをインクルードする(あらかじめ設定→C/C++→プロジェクト オプションに,インクルードファイルのパス(/I"C:\mssdk\include")が通っているのを確認)
  4. COMの初期化と終了処理のために, CoInitialize(NULL), CoUninitialize()を始めと終わりに書く.
  5. 最後に参照カウンタをデクリメントするために (ポインタ)->Release を忘れずに.

#include <iostream>
#include <dshow.h>

using namespace std;

int main()
{
  cout << "Aiko 花火: [Enter]" << endl;
  getchar();

  CoInitialize(NULL);

  IGraphBuilder *pGraph;	
  IMediaControl *pMediaControl;
  IMediaEvent *pEvent;

  CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
      IID_IGraphBuilder, (void **)&pGraph);
  pGraph->QueryInterface(IID_IMediaControl, (void **)&pMediaControl);
  pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

  // グラフを作成
  pGraph->RenderFile(L"D:\\aiko-hanabi.mpg", NULL);

  // グラフの実行
  pMediaControl->Run();

  // 終了を待つ
  long evCode;
  pEvent->WaitForCompletion(INFINITE, &evCode);

  // クリーンアップ
  pMediaControl->Release();
  pEvent->Release();
  pGraph->Release();
  CoUninitialize();

  return;
}

あと,IVideoWindow や IMediaEventEx のサンプルもあるので試す. (WinMain で書かれているが,Dialog with MFC ぐらいにしてみる)

フィルタグラフ・・・フィルタとピン
ストリーム・・・
行く河の流れは絶えずしてしかももとの水にはあらず
うう,よくわからんが,
流れ(stream)を制(control)する者はDirectShowを制する

(グラフ内の)streamが全て絶えたときは EC_COMPLETE イベントがアプリケーションに送られるらしい

DirectShowでビデオキャプチャ

まずは,Help(DirectShow の使い方 → ビデオキャプチャDirectShowチュートリアル → AVIファイルの再圧縮) を参考にビデオキャプチャの 流れをまとめてみよう.

基本的流れ

(最初と最後にそれぞれ CoInitialize(NULL), CoUninitialize()を 呼ぶのを忘れないように・・・)

1. フィルタグラフ作成 CoCreateInstance( CLSID_FilterGraph )
2. キャプチャデバイスの取得 ICreateDevNum, IEnumMoniker, IMoniker, IBaseFilter
3. キャプチャビルダグラフ ICaptureGraphBuilder2
3-1. キャプチャビルダグラフ作成 CoCreateInstance( CLSID_CaptureGraphBuilder2 )
3-2. フィルタグラフへの関連付け ICaptureGraphBuilder2::SetFilterGraph((IFilterGraph)pGraph)
4. ファイルライタフィルタ IFileSinkFilter
4-1. フィルタグラフへの追加 ICaptureGraphBuilder2::SetOutputFileName(...)
4-2. ストリームのレンダリング設定 ICaptureGraphBuilder2::RenderStream(...)
5. キャプチャ開始 IMediaControl::Run()

2のデバイス列挙子の使用に付いては以下にもう少し詳 しくまとめる.

ICaptureGraphBuilder2
一般的なグラフ構築は IGraphBuilderの様々なメソッドを用いて行う. しかし,ビデオキャプチャの場合はデバイスは多様であり, グラフの構築が複雑になりがちである. そこで,アプリケーション開発者の負担軽減のために Capture Graph Builder というオブジェクトが用意されている.一般的なグラフ構築は,Help ( DirectShowについて → フィルタグラフの構築 → 一般的なグラフの構築) 参照.

デバイスの列挙

Help ( DirectShowチュートリアル → デバイスとフィルタの列挙 → システムデバイスの列挙子の使用) 参照

2-1. デバイスの列挙子を取得 ICreateDevEnum CoCreateInstanceによってインスタンス生成
2-2. カテゴリの列挙子を取得 IEnumMoniker ICreateDevEnum::CreateClassEnumerator(カテゴリ名, ...) によって入手
2-3. モニカを取得 IMoniker IEnumMoniker::Next によって列挙
2-4. フィルタにバインド IBaseFilter IMoniker::BindToObject(..., (void **)pSrc)

フレンドリ名を取得するには, 4で IMoniker::BindToStrage を用いてプロパティ バッグを取得し,IPropertyBag::Read を用いるらしい.


ソースリスト (videocap_console)

(注)簡略化のためエラー処理一切なし.
#include <dshow.h>
#include <iostream>

using namespace std;

int main(int argc, char* argv[])
{
  CoInitialize(NULL);

  // 1. フィルタグラフ作成
  IGraphBuilder *pGraph = NULL;
  CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC,
      IID_IGraphBuilder, (void **)&pGraph);

  // 2. システムデバイス列挙子を作成
  ICreateDevEnum *pDevEnum = NULL;
  CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC,
      IID_ICreateDevEnum, (void **)&pDevEnum);

  IEnumMoniker *pClassEnum = NULL;
  pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pClassEnum, 0);

  ULONG cFetched;
  IMoniker *pMoniker = NULL;
  IBaseFilter *pSrc = NULL;
  if (pClassEnum->Next(1, &pMoniker, &cFetched) == S_OK){
    // 最初のモニカをフィルタオブジェクトにバインドする
    pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void **)&pSrc);
    pMoniker->Release();
  }
  pClassEnum->Release();
  pDevEnum->Release();

  pGraph->AddFilter(pSrc, L"Video Capture");

  // 3. キャプチャビルダの作成
  ICaptureGraphBuilder2 *pBuilder = NULL;
  CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC,
      IID_ICaptureGraphBuilder2, (void **)&pBuilder);
  pBuilder->SetFiltergraph(pGraph);
  
  // 4. ファイルライタフィルタの設定
  IBaseFilter *pMux = NULL;
  IFileSinkFilter *pSink = NULL;

  // ファイルへ
  pBuilder->SetOutputFileName(&MEDIASUBTYPE_Avi, L"C:\\dvcap_tmp.avi", &pMux, &pSink);
  pBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pSrc, NULL, pMux);
  REFERENCE_TIME rtStart = 50000000, rtStop = 80000000;
  pBuilder->ControlStream( &PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pSrc, &rtStart, &rtStop,
      0, 0 );
  // ディスプレイへ
  pBuilder->RenderStream( &PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pSrc, NULL, NULL );


  // 5. キャプチャ開始
  IMediaControl *pMediaControl;
  pGraph->QueryInterface(IID_IMediaControl, (void **)&pMediaControl);
  pMediaControl->Run();

  MessageBox(NULL, "Click to stop.", "DirectShow", MB_OK);
  
  // 6. 終了
  pSrc->Release();
  pMux->Release();
  pSink->Release();
  pMediaControl->Release();
  pEvent->Release();
  pBuilder->Release();
  pGraph->Release();
  CoUninitialize();

  cout << "end\n";
  return 0;
}

(注)Preview を見ていると, キャプチャされた画像の色数がやたらと少ない場合(おそらく16bpp)がある.これは, YUVから RGBへ の変換方法に依存するようだ.この辺は,キャプチャ ドライバとグラフィックカードの組合わせによって異なる.

一般には,AVIデコンプレッサというのがこの変換を司るが, グラフィックカードがハードウェア的にサポートしていれば(かつVideoRenderer で YUV Overlays/Flipping/OffScreen等 がオンになっていれば),AVIデコンプレッサはYUVを素通りさせ,直接ビデオメモリのDirectDrawオーバレイサーフェイスにコピーする.グラフィックカードが サポートしていない場合は,AVIデコンプレッサが MSYUV CODECを用いてソフト的に変換を行う.しかし,このMSYUVはRGB16, RGB8へ の変換しかできないらしく,この場合に色数が少なくなる.RGB24,32をサポート するCODECを用いれば良いのだが・・・(例えば,Unibrain Fire-i のキャプチャドライバでは,MSYUV CODECでなく,独自のYUV CODEC (fiyuv.dll, fiyuv.ax(DirectShow Filter)) によってRGB32を実現している)(<--もはや昔 の話です.今のDirectshow9のフィルタはRGB32になります.)

まあ,あくまで表示(VideoRenderer)の話なので,ファイルに落とすとYUVのまま 保存されているようだ.
参照 Help( DirectShowリファレンス → フィルタ → MSYUVカラース ペース変換 CODEC )

フィルタプロパティの取得

フィルタの列挙

Help ( DirectShowチュートリアル → フィルタグラフ内のオブジェクトの列挙 → フィルタの列挙) 参照

1. フィルタ列挙子の取得 IEnumFilters IFilterGraph::EnumFilters
2. フィルタの列挙 IBaseFilter IEnumFilters::Next(1, (IBaseFilter **), (ULONG *))
3. フィルタ名の取得 FILTER_INFO IBaseFilter::QueryFilterInfo(FILTER_INFO *)

フィルタのプロパティページ表示

Help ( DirectShowチュートリアル → フィルタのプロパティページの表示) を参考に・・・
直接生成したフィルタ以外は,フィルタへのポインタを持っていない.そこで, フィルタの列挙子を用いて,順にグラフ内のフィル タ(へのポインタ)を取得する.もしくは,フィルタ名が分かっているときは IFilterGraph::FindFilterByName メソッドを用いる.

4. プロパティページインタフェース取得 ISpecifyPropertyPages IBaseFilter::QueryInterfase(IID_ISpecifyPropertyPages, (void **))
5. プロパティページの取得 CAUUID ISpecifyPropertyPages::GetPages(CAUUID *)
ISpecifyPropertyPages::Release()
6. プロパティページの表示 OleCreatePropertyFrame
7. GUID配列の解放 CoTaskMemFree(CAUUID::pElems)

CAUUID は GUIDのカウント配列が定義される構造体.初期化は要らない.各プロパティページ のクラス識別(CLSID)がCAUUID::pElemsに入る. OleCreatePropertyFrameによって作成されるダイアログはモーダル.

5. キャプチャ開始 の直前にプロパティページを表示させてみてもいいかも.

  // フィルタを列挙し、それらのプロパティ ページを表示する。
  IEnumFilters *pEnum;
  IBaseFilter *pFilter = NULL;

  HRESULT hr;
  pGraph->EnumFilters(&pEnum);
  while(pEnum->Next(1, &pFilter, NULL) == S_OK)
    {
      ISpecifyPropertyPages *pSpecify;
      hr = pFilter->QueryInterface(IID_ISpecifyPropertyPages, (void **)&pSpecify);
      if (SUCCEEDED(hr)) {
        FILTER_INFO FilterInfo;
        pFilter->QueryFilterInfo(&FilterInfo);

        CAUUID caGUID;
        pSpecify->GetPages(&caGUID);
        pSpecify->Release();

        OleCreatePropertyFrame(
          NULL,                   // 親ウィンドウ
          0,                      // x (予約済み)
          0,                      // y (予約済み)
          FilterInfo.achName,     // ダイアログ ボックスのキャプション
          1,                      // フィルタの数
          (IUnknown **)&pFilter,  // フィルタへのポインタ
          caGUID.cElems,          // プロパティ ページの数
          caGUID.pElems,          // プロパティ ページ CLSID へのポインタ
          0,                      // ロケール識別子
          0,                      // 予約済み
          NULL                    // 予約済み
          );
        CoTaskMemFree(caGUID.pElems);
        FilterInfo.pGraph->Release();
      }
      pFilter->Release();
    }
  pEnum->Release();

どうも,ビデオキャプチャーだと動的に決まるパラメータが多いので,キャプチャー 前にモーダルダイアログでプロパティを表示させても意味無いな.後でモード レスなプロパティボックスにしよう.

DirectShowでスチルキャプチャ(その1:GetCurrentBuffer編)

流れのなかに網を仕掛けておくイメージで,上のビデオキャプチャのstream中にサ ンプルグラバを挿入する(ちょっと違うか.).これによって,フィルタを通過 していくサンプルを獲得できるそうだ.これは SampleGrabberフィルタを作成し, ISampleGrabberインタフェースを用いて行うが,次の2つの方法があるらしい.

  1. ISampleGrabber::SetBufferSample(TRUE)にし,ISampleGrabber::GetCurrentBuffer()を明示的に呼ぶ
  2. ISampleGrabberCBインタフェースを実装したクラスを作成し, ISampleGrabber::SetCallBack()によって関連付ける

ここでは,まずは前者を試してみることにしよう.(ちなみにDirectShowサンプルStillCap は後者を用いている)
Help ( DirectShow チュートリアル → メディアサンプルの獲得) 参照.

基本的に,1から3はビデオキャプチャと同じだ が,4を次のようにする.

基本的流れ

4-1. サンプルグラバの生成 IBaseFilter
ISampleGrabber
CoCreateInstance(CLSID_SampleGrabber, ..., IID_IBaseFilter, ...)
IBaseFilter::QueryInterfase(IID_ISampleGrabber, ...)
4-2. メディアタイプの設定 AM_MEDIA_TYPE ISampleGrabber::SetMediaType(AM_MEDIA_TYPE *)
4-3. フィルタグラフへ追加 IFilterGraph::AddFilter(IBaseFilter *)
4-4. サンプルグラバの接続 様々な方法があるらしい
4-5. グラバのモードを適切に設定 ISampleGrabber::SetBufferSample(TRUE) : 毎サンプルをバッファにコピーするかどうか
ISampleGrabber::SetOneShot(FALSE) : 1サンプルでグラフを終了するか
5. キャプチャ中にグラブを行う ISampleGrabber::GetCurrentBuffer(...)

サンプルグラバの接続は・・・次のようなやり方があるらしいが.

  1. IGraphBuilder::RenderFile (メディアタイプにしたがってグラフマネー ジャがサンプルグラバを接続する)
  2. IGraphBuilder::Connect (ピンで指定)
  3. ICaptureGraphBuilder2

1と3は,ある程度自動で構築するみたいやけど,今はどこに接続するかを理解す るのが大切なので,ここでは,2のピンで指定してサンプルグラバフィルタを挿入 するやり方をとる.フィルタを追加した際のピンの接続に関しては,Help ( DirectShow について → フィルタグラフの構築 → 一般的なグラフの構築) 参照.

(注) ISampleGrabberを用いるには, #include<qedit.h>をインクルードする必要がある.


ピンの列挙

まず,指定した方向のピンを得るための関数をつくる. Help ( DirectShowチュートリアル → フィルタグラフ内のオブジェクトの列挙 → ピンの列挙) 参照

1. ピン列挙子の取得 IEnumPins IBaseFilter::EnumPins
2. ピンの列挙 IPin IEnumPins::Next(1, (IPin **), 0)
3. ピンの方向(IN,OUT)の取得 FILTER_INFO IPin::QueryDirection(PIN_DIRECTION *)
IPin *GetPin(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
        break;
      pPin->Release();
    }
  pEnum->Release();
  return (bFound ? pPin : 0);
}

ソースリスト ( stillcap_console1 )

ただし,ISampleGrabber を用いるために #include <qedit.h> を加える.

  // 4. 一枚撮る
  // 4-1. サンプルグラバの生成
  IBaseFilter *pF = NULL;
  ISampleGrabber *pSGrab;
  CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER,
      IID_IBaseFilter, (LPVOID *)&pF);
  pF->QueryInterface(IID_ISampleGrabber, (void **)&pSGrab);

  // 4-2. メディアタイプの設定
  AM_MEDIA_TYPE mt;
  ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
  mt.majortype = MEDIATYPE_Video; // Sample Grabber の入力ピン(Capture Device の出力ピン)はUYVY
  mt.subtype = MEDIASUBTYPE_UYVY;     
  mt.formattype = FORMAT_VideoInfo;
  pSGrab->SetMediaType(&mt);

  // 4-3. フィルタグラフへ追加
  pGraph->AddFilter(pF, L"Grabber");

  // 4-4. サンプルグラバの接続
  // [pSrc](o) -> (i)[pF](o) -> [VideoRender]
  //        ↑A   ↑B     ↑C
  IPin *pSrcOut = GetPin(pSrc, PINDIR_OUTPUT); // A
  IPin *pSGrabIN = GetPin(pF, PINDIR_INPUT);    // B
  IPin *pSGrabOut = GetPin(pF, PINDIR_OUTPUT);  // C

  pGraph->Connect(pSrcOut, pSGrabIN);
  pGraph->Render(pSGrabOut);

  // 4-5. グラバのモードを適切に設定
  pSGrab->SetBufferSamples(TRUE); // GetCurrentBuffer を用いる際には必ずTRUE (FALSE では失敗する)
  pSGrab->SetOneShot(FALSE);      // ワンショットをOFFにする

  // 5. キャプチャ開始
  IMediaControl *pMediaControl = NULL;
  pGraph->QueryInterface(IID_IMediaControl, (void **)&pMediaControl);

  pMediaControl->Run();

  long *buffer = NULL;
  long bufsize = 0;
  // サイズ取得
  do{
    pSGrab->GetCurrentBuffer(&bufsize, NULL);
  }while(bufsize <= 0);
  cerr << "bufsize : " << bufsize << endl;
 	buffer = (long *)malloc(sizeof(char)*bufsize);
  if(buffer==NULL){
    cerr << "can't allocate memory\n";
    return 1;
  }
  // 一枚キャプチャ
  MessageBox(NULL, "Click to capture & stop.", "DirectShow", MB_OK);
  // バッファ取得
  hr = pSGrab->GetCurrentBuffer(&bufsize, buffer);
  if(hr==S_OK){
    cerr << "Capture Succeeded\n";
  }

  // 6. 終了
  pF->Release();      // こいつらも
  pSGrab->Release();  // わすれずに解放
  pSrc->Release();
  pMediaControl->Release();
  pBuilder->Release();
  pGraph->Release();
  CoUninitialize();

メディアタイプはHelp( DirectShow リファレンス → 定数とGUID → メディアタイプ )参照.

DirectShowでスチルキャプチャ(その2:コールバック編)

基本的流れ

基本的に先のスチルキャプチャと同様であるが,受け取るサンプル毎にコールバッ クを呼び出すことができるので,これを利用してバッファの内容をファイルに落 とすことにする.コールバック関数としては SampleCBかBufferCBを実装せなあ かんらしいが,両者の引数を見てみると,その違いはこんな感じやと思われる.

ここでは,より単純やと思われる後者を実装する.

0. ISampleGrabberCBインタフェースの実装 ISampleGrabberCB メソッドとしてSampleCBかBufferCBのいずれか一つを実装する
4-5. グラバのモードを適切に設定 SetBufferSample(FALSE), SetOneShot(FALSE)
SetCallback() : コールバックメソッドを指定
第1引数:ISampleGrabberCB実装クラスのインスタンスへのポインタ
第2引数:どちらのメソッドを用いるか.0ならSampleCB, 1ならBufferCB

Help( DirectShowチュートリアル → メディアサンプルの獲得 → コールバックメソッ ドの利用 )参照
(注)streams.hをインクルードし,strmbase.lib(Release)もしくは strmbasd.lib(Debug)をリンクする必要がある.


コールバックインタフェースの実装

ISampleGrabberCBインタフェースを継承し,メソッドを実装する.ただし,これ はCOMに基づいて実装する必要があるため,簡略化のためCUnknown も継承する. これによって,IUnknown のAddRef, Release, QueryInterfaceといったメソッド を1から実装する手間が省ける.その代わり,NonDelegatingQueryInterfaceを実装す る.(サンプル StillCap では,CUnknownは継承せず1から実装している)


class CGrabCB: public CUnknown, public ISampleGrabberCB
{
public:
  DECLARE_IUNKNOWN;

  STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void **ppv)
  {
    if( riid == IID_ISampleGrabberCB ){
      return GetInterface((ISampleGrabberCB*)this, ppv);
    }
    return CUnknown::NonDelegatingQueryInterface(riid, ppv);
  }

  // ISampleGrabberCB のメソッド
  STDMETHODIMP SampleCB(double SampleTime, IMediaSample *pSample)
  {
    return E_NOTIMPL;
  }

  STDMETHODIMP BufferCB(double SampleTime, BYTE *pBuffer, long BufferLen)
  {
    cerr << "Sample time: " << SampleTime  << "\t";
    cerr << "BufferLen: " << BufferLen;
    cerr << endl;
    SaveOneImage("C:\\tmp.yuv", pBuffer, BufferLen); // 後に定義する
    return S_OK;
  }
  // コンストラクタ
  CGrabCB( ) : CUnknown("SGCB", NULL)
    {	}

};

インプリメントしないメソッド(この場合はSampleCB)では E_NOTIMPL を返すよ うにする.


ソースリスト ( stillcap_console2 )

まずSaveOneImageを定義しておく.これはsaveflagがTRUEになる度に,一回だ けバッファ内容をファイルに書き出す関数である.


BOOL saveflag=FALSE;

inline BOOL SaveOneImage(const char *fname, BYTE *pBuffer, long BufferLen)
{
  if(saveflag){
    ofstream fout(fname, ios::out | ios::binary);
    if(!fout.is_open()) return FALSE;
    fout.write((char *)pBuffer, BufferLen);
    fout.close();
    saveflag=FALSE;
  }
  return TRUE;
}

次に,stillcap_cosole1のmain関数内で変更する部分を示す.


  // 4-5. グラバのモードを適切に設定
  pSGrab->SetBufferSamples(FALSE);
  pSGrab->SetOneShot(FALSE);
  CGrabCB *cb = new CGrabCB();
  pSGrab->SetCallback(cb, 1);  // 第2引数でコールバックを指定 (0:SampleCB, 1:BufferCB)

  // 5. キャプチャ開始
  IMediaControl *pMediaControl = NULL;
  IMediaEvent *pEvent = NULL;

  pGraph->QueryInterface(IID_IMediaControl, (void **)&pMediaControl);
  pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

  long evCode;
  pMediaControl->Run();
  MessageBox(NULL, "Click to capture & stop.", "DirectShow", MB_OK);
  saveflag = TRUE;
  pEvent->WaitForCompletion(100, &evCode);

CGrabCB のインスタンスを作成し,SetCallbackで設定しているぐらいやね. MessageBoxにOKしたあとにsaveflagをTRUEにする.すると,SaveOneImageはその ときのバッファの内容をファイルを保存する.コールバックはスレッドで呼ばれ るため,WaitForCompletionを用いて保存する時間を確保している(ばかちょん でやけど).

カメラのコントロール

基本的流れ

ビデオデバイスのドライバが, WDM (Windows Deriver Model) に基づいて書か れたビデオキャプチャフィルタであるとする.これが IAMCameraControlをサポー トしていれば,次のような手順でzoom,focus・・・などのパラメタをコントロー ルできるはず.同様に,IAMVideoProcAmpを用いて,輝度,コントラスト,色相, 彩度,ガンマ,鮮明度などを調節できる.

a. デバイスのインタフェイス取得 IAMCameraControl, IAMVideoProcAmp IBaseFilter::QueryInterface(...)
b. パラメタの範囲取得 GetRange(...)
c. パラメタコントロール Set(...)
b. パラメタ取得 Get(...)
WDMドライバとKsProxyフィルタ
KsProxyフィルタというのがある.Kernel Streaming Proxyの略らしい.細かい ことは省略するが(ていうか理解できていないが),要するに,カーネルモード のKSドライバ(WDMドライバの一種)によって動いているデバイスを,ユーザモー ドのDirectShowフィルタとして見せかけ,アプリケーションレベルでコントロールするための ラッパフィルタらしい.WDMに準拠したデバイスと,適切に書かれたドライバ (正確にはmini driver)の組み合わせであれば,ksproxyフィルタによってラップできる.
で,Kernel Streaming は何がいいかというと,「データをユーザモードに渡すことなく,完全にカーネルモードで 直接ストリーミングすることができる」ので,大幅にCPUの負荷を減らす ことができるらしい.
KSドライバとKsProxyフィルタの関係は,おそらくこんな感じやろ(想像図)

ソースリスト (cameracontrol)

これはほとんどvideocapture_consoleその まま.ただし,ファイル出力部分を削除した.

   .....

#define NSTEP 5
   .....

  // 3. 
  ICaptureGraphBuilder2 *pBuilder = NULL;
  CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC,
      IID_ICaptureGraphBuilder2, (void **)&pBuilder);
  pBuilder->SetFiltergraph(pGraph);
  pBuilder->RenderStream( &PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pSrc, NULL, NULL );

  // カメラコントロール
  IAMCameraControl *pCameraControl;
  CameraControlProperty prop;
  pSrc->QueryInterface(IID_IAMCameraControl, (void **)&pCameraControl);
  long Min, Max, SteppingDelta, Default, CapsFlags;

  prop = CameraControl_Zoom;
  pCameraControl->GetRange(prop, &Min, &Max, &SteppingDelta, &Default, &CapsFlags);
  cerr << "Min : " << Min << endl;
  cerr << "Max : " << Max << endl;
  cerr << "Stepping Delta : " << SteppingDelta << endl;
  cerr << "Default : " << Default << endl;
  cerr << "CapsFlags : " << CapsFlags << endl;  long zoom;
  long cflags;
  pCameraControl->Get(prop, &zoom, &cflags);
  cerr << "zoom : " << zoom << endl;

  // 5. キャプチャ
  IMediaControl *pMediaControl;
  pGraph->QueryInterface(IID_IMediaControl, (void **)&pMediaControl);
  pMediaControl->Run();
  long setzoom=Min;
  long step = (Max-Min)/NSTEP;
  cerr << "step : " << step << endl;
  for(int i=0; i < NSTEP; i++){
    if(i==NSTEP-1) setzoom = Min; // zoom:Minで終える
    MessageBox(NULL, "Click to zoom up", "DirectShow", MB_OK);
    pCameraControl->Set(prop, setzoom, cflags);
    int diff;
    do{
      pCameraControl->Get(prop, &zoom, &cflags);
      cerr << "zoom : " << zoom << endl;
      diff = zoom - setzoom;
      cerr << "diff*diff: " << diff*diff << endl;
    }while(diff*diff > 1);
    setzoom += step;

  }
  
  MessageBox(NULL, "Click to stop.", "DirectShow", MB_OK);

  // 6. 
  pCameraControl->Release();
  pSrc->Release();
  .....

番外編

自分のフィルタグラフをGraphEditで確認

便利やし,ヘルプそのままやけどメモっとこ.自分の書いたアプリケーションで, フィルタグラフがどのような構成になっているかを,GraphEditを用いて目で確 認できる.GraphEditからプリントしてpsファイル等に落とせば,ドキュメント にもしやすいし.

GraphEditでの表示方法
以下の様にAddToRot, RemoveFromRot をソースに追加し,アプリケーション実行 中に,GraphEditの [File] → [Connect] で アプリケーションのpidを選択する.
終了方法
念のため,GraphEditを終了してから,アプリケーションを終了すること.へル プには,「アプリケーションでは,終了時にさまざまなエラーが発生することが ある.」とある.また,このときのGraphEditでは,フィルタの追加やグラフの停 止など,いらんことはせんほうがええらしい.あくまで見るだけ.

HRESULT AddToRot(IUnknown *pUnkGraph, DWORD *pdwRegister) 
{
    IMoniker * pMoniker;
    IRunningObjectTable *pROT;
    if (FAILED(GetRunningObjectTable(0, &pROT))) {
        return E_FAIL;
    }
    WCHAR wsz[256];
    wsprintfW(wsz, L"FilterGraph %08p pid %08x", (DWORD_PTR)pUnkGraph, GetCurrentProcessId());
    HRESULT hr = CreateItemMoniker(L"!", wsz, &pMoniker);
    if (SUCCEEDED(hr)) {
        hr = pROT->Register(0, pUnkGraph, pMoniker, pdwRegister);
        pMoniker->Release();
    }
    pROT->Release();
    return hr;
}

void RemoveFromRot(DWORD pdwRegister)
{
    IRunningObjectTable *pROT;
    if (SUCCEEDED(GetRunningObjectTable(0, &pROT))) {
        pROT->Revoke(pdwRegister);
        pROT->Release();
    }
}

  ・・・

  // 1. フィルタグラフ作成
  IGraphBuilder *pGraph = NULL;
  CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC, IID_IGraphBuilder, (void **)&pGraph);
  // FilterGraph作成後にRot(実行時オブジェクトテーブル)に追加
  DWORD dwRegister;
  hr = AddToRot(pGraph, &dwRegister);

  ・・・

  // Graphのリリース前にRotから削除
  RemoveFromRot(dwRegister);
  pGraph->Release();

  ・・・

ちなみに videocap_consoleはこんなグラフになった.


コンソールウインドウを消す

これも一応メモっとこっと.
毎回毎回 「Press any key to continue」に答えるのは面倒.もしコンソールが 必要ないなら,[設定] → [リンク] → [プロジェクトオプション] の最後に

/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup

を加える.

複数カメラでのキャプチャ

基本的流れ

カメラが複数になっても,一つのグラフに追加できるみたいやな. とりあえずやってみよう. ただし,今はカメラ間の同期などは考えないものとする.


ソースリスト ( two_cameras )


・・・

#define NCAM 2

int main(int argc, char* argv[])
{
  HRESULT hr;
  CoInitialize(NULL);

  // 1. フィルタグラフ作成
  IGraphBuilder *pGraph = NULL;
  CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC,
      IID_IGraphBuilder, (void **)&pGraph);
  DWORD dwRegister;
  hr = AddToRot(pGraph, &dwRegister);

  // 2. システムデバイス列挙子を作成
  ICreateDevEnum *pDevEnum = NULL;
  CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC,
      IID_ICreateDevEnum, (void **)&pDevEnum);
  IEnumMoniker *pClassEnum = NULL;
  pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pClassEnum, 0);
  ULONG cFetched;
  IMoniker *pMoniker = NULL;
  IBaseFilter *pSrc[NCAM];
  int i;
  for(i=0; i < NCAM; i++){
    if (pClassEnum->Next(1, &pMoniker, &cFetched) == S_OK){
      // NCAM番目のモニカまでフィルタオブジェクトにバインドする
      pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void **)&pSrc[i]);
      pMoniker->Release();
    }
    pGraph->AddFilter(pSrc[i], L"Video Capture");
  }
  pClassEnum->Release();
  pDevEnum->Release();

  // 3. キャプチャビルダの作成
  ICaptureGraphBuilder2 *pBuilder[NCAM];
  for(i=0; i < NCAM; i++){
    CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC,
        IID_ICaptureGraphBuilder2, (void **)&pBuilder[i]);
    pBuilder[i]->SetFiltergraph(pGraph);
    // ディスプレイへ
    pBuilder[i]->RenderStream( &PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pSrc[i], NULL, NULL );
  }

  // 5. キャプチャ開始
  IMediaControl *pMediaControl;
  pGraph->QueryInterface(IID_IMediaControl, (void **)&pMediaControl);
  pMediaControl->Run();
  MessageBox(NULL, "Click to stop.", "DirectShow", MB_OK);

  // 6. 終了
  for(i=0; i < NCAM; i++){
    pSrc[i]->Release();
    pBuilder[i]->Release();
  }
  pMediaControl->Release();
  RemoveFromRot(dwRegister);
  pGraph->Release();

  CoUninitialize();
  return 0;
}


フィルタグラフ

GraphEditで表示させると図のようになった.いけとるみたいやん.というわけ で終了.