USBカメラをC#で使おう
作成日: 2003/10/02 最終更新日: 2005/1/18



◆概要

USBカメラをWindows環境で利用するためには,主に二種類の方法があります.一つはVideo for Windowsを利用する方法,もう一つはDirect Showを利用する方法です.今回は,後者のDirectShowを利用して,C#でUSBカメラを制御する方法を紹介します.このプログラムは,以下のような機能を持っています.

  1. USBカメラのプレビュー
  2. USBカメラから静止画をキャプチャ&保存
  3. USBカメラから動画をキャプチャ&保存
  4. キャプチャした動画の再生
  5. USBカメラのプロパティを変更(色調,ズームなど)

ここではまず,DirectShowの基本的な用語などをまとめ,次に機能ごとにプログラムの大まかな流れを説明していきます.より詳しい内容については,サンプルコードや参考URLを参照してください.

なお,動作確認環境は以下のとおりです.他のUSBカメラでもおそらく動作すると思いますが,未確認です.

WindowsXP + .Net Framework1.1J + DirectX9 + Logicool QCam QV-640N(USBカメラ)

図1: USBカメラキャプチャソフトのサンプル

(映っているのはのぽぽんです.)



◆ダウンロード

実行ファイル (zip)
サンプルコード (zip)

 ※このソフトを利用するには,.Net FramworkとDirectX 8.1以上をインストールしておく必要があります.
   -.Net Framwork1.1の再頒布パッケージはこちらからダウンロードできます.
   -DirectX9.0の再頒布パッケージは,こちらからダウンロードできます.

 ※サンプルコードをコンパイルする前に,参照パスの設定を行う必要があります.ソリューションエクスプローラーから,DirectShowTest(プロジェクト名)を選択し,右クリックからプロパティ→参照パスを開き,"."(カレントディレクトリ)をパスに追加してください.この設定は保存時に絶対パスに変換されてしまうため,環境ごとに設定する必要があるようです.(2004/10/25追加)




◆DirectShowNet
DirectX9からは,C#用のクラスライブラリが標準添付されていますが,DirectShowだけはなぜか利用することができません.そこで,ここではDirectShowNetという,.Net用にDirectShowをラップしたライブラリを利用します.DirectShowNetでは,DirectShowの多くの機能をC#から利用することができます.
ただし,一部省略されているクラス・インタフェースも存在します.本ページの説明はDirectShowNetを利用したものです.



◆DirectShowの用語集

DirectShowでは独特の用語が多数用いられており,大変とっつきにくいものになっています.ここでは,DirectShowでプログラミングを行う際に最低限知っておく必要がある用語について説明します.各項目の[ ]内の用語は,関連するDirectShowNet内の代表的なクラスやインタフェースです.

  • フィルタ [IBaseFilter]
      マルチメディア ストリーム(データ)に対して一つの操作を行うコンポーネント.
      大きく分けると,以下の三種類となる.
      • ソースフィルタ
          データをファイルやUSBカメラなどから取得し、次のフィルタに送る。
      • 変換フィルタ
          データを受け取り,圧縮やエフェクト処理を行い,次のフィルタに送る.
      • レンダラフィルタ
        • データを受け取り,ディスプレイやファイルに出力する.
  • フィルタグラフ(グラフ)
    • 特定のタスクを行うために接続されたフィルタの集合体
      ex.)AVI ファイルを再生する場合のフィルタグラフ の一例
      1. ファイルソース (ファイルからデータを読み込む)
      2. AVIスプリッタ (オーディオデータとビデオデータを分離する)
      3. AVIデコンプレッサ(ビデオデータの圧縮コーデックに応じた処理を行う)
      4. ビデオレンダラ (データを画面上に出力する)
  • フィルタ グラフ マネージャ [IGraphBuilder, IMediaControl, IMediaEventEx]
      フィルタグラフを作成したり,フィルタグラフを流れるデータの流れを制御する,上位コンポーネント.
      ex.) IMediaControl.Run (グラフの開始から終わりまでデータを移動する)
  • キャプチャグラフビルダ [ICaptureGraphBuilder2]
      ビデオキャプチャや編集用のメソッドを備えたコンポーネント.フィルタグラフマネージャに接続して利用する.
  • ピン
    • フィルタが,他のフィルタの接続する部分の呼称.(入力ピン,出力ピン)
      全てのフィルタは,ピンを介して他のフィルタに接続する,と考える.



◆ 静止画のキャプチャ
では,USBカメラから静止画をキャプチャする手順を,「準備編」「実行編」に分けて説明します.詳細については,ソースコードを参照してください.
※エラー処理などは記述していません.また,説明の都合により,ソースコードと一部記述順序が異なります.
(準備編)

 

  1. キャプチャデバイス(USBカメラ)を選択する.

      private ArrayList captureDevices;
      DsDevice device = null;
      //PCに接続されているキャプチャデバイス(USBカメラなど)のリストを取得.
      if(!DsDev.GetDevicesOfCat(FilterCategory.VideoInputDevice, out captureDevices))
      {
      MessageBox.Show("ビデオキャプチャ可能なデバイスが見つかりません.");
      return;
      }
      //最初に見つかったデバイスを選択
      device = captureDevices[0] as DsDevice;
  2. キャプチャデバイスをソースフィルタに対応づける.

      private IBaseFilter captureFilter; //ソースフィルタ
      object captureObject = null;

      //キャプチャデバイス(device)とソースフィルタ(captureFilter)を対応付ける.
      Guid guidBF = typeof(IBaseFilter).GUID;
      device.moniker.BindToObject(null, null, ref guidBF, out captureObject);
      captureFilter = (IBaseFilter) captureObject;

  3. フィルタグラフマネージャを作成し,各種操作を行うためのインタフェースを取得する.


      private IGraphBuilder graphBuilder;
      //基本的なフィルタグラフマネージャ

      private IVideoWindow videoWindow; //オーナーウィンドウの位置やサイズなどの設定用のインタフェース.

      private IMediaControl mediaControl;//データのストリーミングの移動、ポーズ、停止などの処理用のインタフェース.
      private IMediaEventEx mediaEvent; //DirectShowイベント処理用のインタフェース


      //graphBuilderを作成.
      comType = Type.GetTypeFromCLSID(Clsid.FilterGraph);

      comObject = Activator.CreateInstance(comType);
      graphBuilder = (IGraphBuilder) comObject;
      comObject = null;

      //各種インタフェースを取得.
      mediaControl = (IMediaControl) graphBuilder;
      videoWindow = (IVideoWindow) graphBuilder;
      mediaEvent = (IMediaEventEx) graphBuilder;

  4. キャプチャグラフビルダと,サンプルグラバフィルタ(個々のビデオデータを取得するフィルタ)を作成する.

    private ICaptureGraphBuilder2 captureGraphBuilder;//ビデオキャプチャ&編集用のメソッドを備えたキャプチャグラフビルダ
    private ISampleGrabber sampleGrabber; //フィルタグラフ内を通る個々のデータ取得用のインタフェース.
    private IBaseFilter grabFilter; //Grabber Filterのインタフェース.
    int result; //エラー判定用フラグ

    //キャプチャグラフビルダ(captureGraphBuilder)を作成.
    comType = Type.GetTypeFromCLSID(Clsid.CaptureGraphBuilder2);
    comObject = Activator.CreateInstance(comType);
    captureGraphBuilder = (ICaptureGraphBuilder2) comObject;
    comObject = null;

    //サンプルグラバ(sampleGrabber)を作成
    comType = Type.GetTypeFromCLSID(Clsid.SampleGrabber);
    comObject = Activator.CreateInstance(comType);
    sampleGrabber = (ISampleGrabber) comObject;
    comObject = null;

    //フィルタと関連付ける.
    grabFilter = (IBaseFilter) sampleGrabber;

    //キャプチャするビデオデータのフォーマットを設定.
    AMMediaType amMediaType = new AMMediaType();
    amMediaType.majorType = MediaType.Video;
    amMediaType.subType = MediaSubType.RGB24;
    amMediaType.formatType = FormatType.VideoInfo;

    result = sampleGrabber.SetMediaType(amMediaType);
    if(result < 0) Marshal.ThrowExceptionForHR(result);

  5. 基本となるフィルタグラフマネージャ(graphBuilder)にキャプチャグラフビルダと各フィルタを追加する.

    //captureGraphBuilder(キャプチャグラフビルダ)をgraphBuilder(フィルタグラフマネージャ)に追加.
    result = captureGraphBuilder.SetFiltergraph(graphBuilder);
    if(result < 0) Marshal.ThrowExceptionForHR(result);

    //captureFilter(ソースフィルタ)をgraphBuilder(フィルタグラフマネージャ)に追加.
    result = graphBuilder.AddFilter(captureFilter, "Video Capture Device");
    if(result < 0) Marshal.ThrowExceptionForHR(result);

    //grabFilter(変換フィルタ)をgraphBuilder(フィルタグラフマネージャ)に追加.
    result = graphBuilder.AddFilter(grabFilter, "Frame Grab Filter");
    if(result < 0) Marshal.ThrowExceptionForHR(result);

  6. 各フィルタの接続を行い,入力画像のキャプチャとプレビューの準備を行う.

    VideoInfoHeader videoInfoHeader; //ビデオイメージのフォーマットを記述する構造体
    Guid pinCategory;
    Guid mediaType;

    //キャプチャフィルタをサンプルグラバーフィルタに接続する.
    pinCategory = PinCategory.Capture;
    mediaType = MediaType.Video;
    result = captureGraphBuilder.RenderStream(ref pinCategory, ref mediaType,
    captureFilter, null, grabFilter);
    if(result < 0) Marshal.ThrowExceptionForHR(result);

    //キャプチャフィルタをデフォルトのレンダラフィルタ(ディスプレイ上に出力)に接続する.(プレビュー)
    pinCategory = PinCategory.Preview;
    mediaType = MediaType.Video;
    result = captureGraphBuilder.RenderStream(ref pinCategory, ref mediaType,
    captureFilter, null, null);
    if(result < 0) Marshal.ThrowExceptionForHR(result);

    //フレームキャプチャの設定が完了したかを確認する.
    amMediaType = new AMMediaType();
    result = sampleGrabber.GetConnectedMediaType(amMediaType);
    if(result < 0) Marshal.ThrowExceptionForHR(result);
    if((amMediaType.formatType != FormatType.VideoInfo) || (amMediaType.formatPtr == IntPtr.Zero))
    throw new NotSupportedException("キャプチャできないメディアフォーマットです.");

    //キャプチャするビデオデータのフォーマットから,videoInfoHeaderを作成する.
    videoInfoHeader =
    (VideoInfoHeader) Marshal.PtrToStructure(amMediaType.formatPtr, typeof(VideoInfoHeader));
    Marshal.FreeCoTaskMem(amMediaType.formatPtr);
    amMediaType.formatPtr = IntPtr.Zero;

    //フィルタ内を通るサンプルをバッファにコピーしないように指定する.
    result = sampleGrabber.SetBufferSamples(false);
    //サンプルを一つ(1フレーム)受け取ったらフィルタを停止するように指定する.
    if(result == 0) result = sampleGrabber.SetOneShot(false);
    //コールバック関数の利用を停止する.
    if(result == 0) result = sampleGrabber.SetCallback(null, 0);
    if(result < 0) Marshal.ThrowExceptionForHR(result);

  7. プレビュー映像(レンダラフィルタの出力)の出力場所を設定する.

    • Panel videoPanel;
      private const int WS_CHILD = 0x40000000;
      private const int WS_CLIPCHILDREN = 0x02000000;

      //プレビュー映像を表示するパネルを指定.
      result = videoWindow.put_Owner(videoPanel.Handle);
      if(result < 0) Marshal.ThrowExceptionForHR(result);
      //ビデオ表示領域のスタイルを指定.
      result = videoWindow.put_WindowStyle(WS_CHILD | WS_CLIPCHILDREN);
      if(result < 0) Marshal.ThrowExceptionForHR(result);

      //ビデオパネルのサイズを変更する.
      Rectangle rect = videoPanel.ClientRectangle;
      videoWindow.SetWindowPosition(0,0,rect.Right,rect.Bottom);

      //レンダラフィルタの出力を可視化する.
      result = videoWindow.put_Visible(DsHlp.OATRUE);
      if(result < 0) Marshal.ThrowExceptionForHR(result);

  8. DirectShowイベントを,Windowsメッセージを通して通知するための設定を行う.

      public const int WM_GRAPHNOTIFY = 0x00008001;//DirectShowイベントの発生を表すWindows メッセージ.

      //mediaEvent(DirectShowイベント)をWM_GRAPHNOTIFY(Windowsメッセージ)に対応付ける.
      result= mediaEvent.SetNotifyWindow(this.Handle, WM_GRAPHNOTIFY, IntPtr.Zero);
      if(result < 0) Marshal.ThrowExceptionForHR(result);

  9. プレビューを開始する.

      result = mediaControl.Run();
      if(result < 0) Marshal.ThrowExceptionForHR(result);
(実行編)
  1. キャプチャするフレームデータのサイズを設定する.
      int size = videoInfoHeader.BmiHeader.ImageSize;
      frameArray = new byte [size + 64000];
  2. サンプルグラバーのコールバックメソッド を有効にし,サンプリングを開始する.
      bool capturedFlag = false;

      //ビデオデータのサンプリングに利用するコールバック メソッドを指定する.
      result = sampleGrabber.SetCallback( this, 1 );

  3. サンプリング完了後,メモリ内のデータを配列にコピーし,CaptureDoneデリゲードを実行する.
      /* 以下は,コールバック関数内の処理.サンプリングが完了すると呼び出される.
      int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer, int BufferLength){ }
      */

      capturedFlag = true;
      //メモリ内のサンプリングされたデータ(pBuffer)を配列(frameArray)にコピーする.
      if((pBuffer != IntPtr.Zero) && (BufferLength > 1000) && (BufferLength <= frameArray.Length))
      Marshal.Copy(pBuffer, frameArray, 0, BufferLength);
      //CaptureDoneデリゲードを呼び出す.
      this.BeginInvoke(new CaptureDone(this.OnCaptureDone));

  4. キャプチャしたデータの配列をメモリ空間内に固定し,ビットマップに変換する.

      /*以下はデリゲード内の処理.
      void OnCaptureDone(){ }
      */
      int result;

      //コールバック関数の利用を停止する.
      result = sampleGrabber.SetCallback(null, 0);

      //フレームデータのサイズを取得
      int width = videoInfoHeader.BmiHeader.Width;
      int height = videoInfoHeader.BmiHeader.Height;

      //widthが4の倍数でない場合&widthとheightの値が適正でない場合は終了.
      if( ((width & 0x03) != 0) || (width < 32) || (width > 4096) || (height < 32) || (height> 4096) )
      return;

      //stride(1ライン分のデータサイズ(byte)=width* 3(RGB))を設定.
      int stride = width * 3;

      //配列frameArrayのアドレスを,メモリ空間内で固定する.
      GCHandle gcHandle = GCHandle.Alloc(frameArray, GCHandleType.Pinned);

      int addr = (int) gcHandle.AddrOfPinnedObject();
      addr += (height - 1) * stride;

      //frameArrayを格納したメモリアドレスから,ビットマップデータを作成.
      Bitmap bitmap = new Bitmap(width,height,-stride,PixelFormat.Format24bppRgb, (IntPtr) addr);
      capturedBitmap = new Bitmap(width,height,-stride,PixelFormat.Format24bppRgb, (IntPtr) addr);
      gcHandle.Free();
      frameArray = nul

  5. 最後に,ビットマップを画面上に表示する
      //画面上にキャプチャした画像を表示する.
      pre = parentForm.PictureBoxImage;
      parentForm.PictureBoxImage = bitmap;
      if(pre != null) pre.Dispose();

      //更新完了.
      updatedFlag = true;



◆動画のキャプチャ
動画のキャプチャは,(圧縮などを考えなければ)静止画のキャプチャよりも簡単です.また,多くのコードが静止画のキャプチャと共通するため,異なる部分のみ説明します.

※今回紹介する方法は,圧縮用のフィルタを利用していないため,キャプチャした動画のファイルサイズが非常に大きくなる点にご注意ください.(320*240,10秒間で100M程度)

(準備編&実行編)

  1. キャプチャデバイス(USBカメラ)を選択する.
  2. キャプチャデバイスをソースフィルタに対応づける.
  3. フィルタグラフマネージャを作成し,各種操作を行うためのインタフェースを取得する.
    これらについては,静止画をキャプチャすると同様なので,省略します.
  4. キャプチャグラフビルダを取得するフィルタを作成する. (※サンプルグラバは不要.)
      private ICaptureGraphBuilder2 captureGraph;//ビデオキャプチャ&編集用のメソッドを備えたキャプチャグラフビルダ

      int result; //エラー判定用フラグ

      //キャプチャグラフビルダ(captureGraphBuilder)を作成.
      comType = Type.GetTypeFromCLSID(Clsid.CaptureGraphBuilder2);
      comObject = Activator.CreateInstance(comType);
      captureGraphBuilder = (ICaptureGraphBuilder2) comObject;
      comObject = null;
  5. 基本となるフィルタグラフマネージャ(graphBuilder)にキャプチャグラフビルダを追加する. (※サンプルグラバは不要.)
      //captureGraphBuilder(キャプチャグラフビルダ)をgraphBuilder(フィルタグラフマネージャ)に追加.
      result = captureGraphBuilder.SetFiltergraph(graphBuilder);
      if(result < 0) Marshal.ThrowExceptionForHR(result);

      //captureFilter(ソースフィルタ)をgraphBuilder(フィルタグラフマネージャ)に追加.
      result = graphBuilder.AddFilter(captureFilter, "Video Capture Device");
      if(result < 0) Marshal.ThrowExceptionForHR(result);
  6. フィルタの接続を行い,入力画像のキャプチャ(動画)とプレビューの準備を行う. 同時に,キャプチャした動画を一時的に保存ためのファイル名を指定する.

      const string DummyFileName = "./tmp.avi";

      IBaseFilter muxFilter = null; //複数の入力ストリームを, AVI フォーマットに変換する.
      IFileSinkFilter sinkFilter = null; //メディア ストリームをファイルに書き込むフィルタ.
      Guid pinCategory;
      Guid mediaType;

      //AVI形式のビデオキャプチャ
      Guid mediaSubType = MediaSubType.Avi;

      //MUXフィルタとファイルライトフィルタを接続し,キャプチャするデータの出力ファイル名を設定.
      result = captureGraphBuilder.SetOutputFileName(ref mediaSubType, DummyFileName,
      out muxFilter, out sinkFilter);
      if(result < 0) Marshal.ThrowExceptionForHR(result);

      //キャプチャフィルタをMUXフィルタに接続する.
      pinCategory = PinCategory.Capture;
      mediaType = MediaType.Video;
      result = captureGraphBuilder.RenderStream(ref pinCategory, ref mediaType,
      captureFilter, null, muxFilter);
      if(result < 0) Marshal.ThrowExceptionForHR(result);

      //キャプチャフィルタをデフォルトのレンダラフィルタ(ディスプレイ上に出力)に接続する.(プレビュー)
      pinCategory = PinCategory.Preview;
      mediaType = MediaType.Video;
      result = captureGraphBuilder.RenderStream(ref pinCategory, ref mediaType,
      captureFilter, null, null);
      if(result < 0) Marshal.ThrowExceptionForHR(result);

  7. プレビュー映像(レンダラフィルタの出力)の出力場所を設定する.
    こちらについては,静止画をキャプチャすると同様なので,省略します.
  8. プレビューと録画を同時に開始する.

    result = mediaControl.Run();
    if(result < 0) Marshal.ThrowExceptionForHR(result);



◆キャプチャした動画の再生
次に,キャプチャした動画をファイルから再生する方法を説明します.これは,今回紹介した三つの中では最も基本的で,簡単なものです.(Microsoftのドキュメントでは,「DirectShowのHello World」とうたわれています.) こちらも,静止画のキャプチャと共通するコードについては省略します.

(準備編&実行編)

  1. フィルタグラフマネージャを作成し,各種操作を行うためのインタフェースを取得する.
    こちらについては,静止画をキャプチャするの3.と同様なので,省略します.
  2. ファイルから動画を読み込み,デフォルトのレンダラフィルタ(ディスプレイ上に出力)に接続する.

      int result = graphBuilder.RenderFile(CaptureForm.DummyFileName, null);
      if(result < 0) Marshal.ThrowExceptionForHR(result);
  3. プレビュー映像(レンダラフィルタの出力)の出力場所を設定する.
    こちらについては,静止画をキャプチャするの7.と同様なので,省略します.
  4. 再生を開始する.

      result = mediaControl.Run();
      if(result < 0) Marshal.ThrowExceptionForHR(result);
  5. ファイルの読み込みが完了するまで待機する.

      int code;
      mediaEvent.WaitForCompletion(1000, out code);



◆おまけ: USBカメラのプロパティ

DirectShowでは,USBカメラのプロパティを簡単に制御することも可能です.制御できるプロパティには,(1)ストリーム形式の設定(図2)と,(2)画像補正系の設定(図3)の二種類があるようです.
サンプルプログラムでは,「基本設定」ボタンで(1)のストリーム形式の設定画面が,「詳細設定」ボタンで(2)の画像補正系の設定画面が開きます.

図2: ストリーム形式のプロパティ

図3: 画像補正系のプロパティ

-ストリーム形式のプロパティ

  1. DsUtils.ShowCapPinDialog()を実行する.
    ※キャプチャグラフビルダー,キャプチャフィルターの設定については,静止画のキャプチャを参照.

    DsUtils.ShowCapPinDialog(captureGraphBuilder, captureFilter, this.Handle);

-画像補正系のプロパティ

  1. キャプチャフィルタからプロパティページへのアクセス用インタフェースを作成する.

      private ISpecifyPropertyPages specifyPropertyPages;//USBカメラの画像補正系設定ページ

      //キャプチャフィルタからspecifyPropertyPageを取得
      specifyPropertyPages = (ISpecifyPropertyPages) captureFilter;

  2. DllImportを用いて,プロパティページを開くためのWin32APIを定義する.
      // ---------------- DLL Imports --------------------
      [DllImport("olepro32.dll", CharSet=CharSet.Unicode, ExactSpelling=true) ]
      private static extern int OleCreatePropertyFrame(
      IntPtr hwndOwner, int x, int y,
      string lpszCaption, int cObjects,
      [In, MarshalAs(UnmanagedType.Interface)] ref object ppUnk,
      int cPages, IntPtr pPageClsID, int lcid, int dwReserved, IntPtr pvReserved);


  3. 画像補正系のプロパティページを開く.

      DsCAUUID cauuid = new DsCAUUID();

      int handle = specifyPropertyPages.GetPages( out cauuid );
      if ( handle != 0 ) Marshal.ThrowExceptionForHR( handle);
       object o = specifyPropertyPages;
        handle = OleCreatePropertyFrame( this.Handle, 30, 30, null, 1,
              ref o, cauuid.cElems, cauuid.pElems, 0, 0, IntPtr.Zero );



 

◆参考URL


[俄プログラマー心得]