USB カメラプログラムに関する Tips

[戻る]

まくら

VC++ でUSB カメラを使って画像プログラムを組む必要があって ちょっと調べてみました. いろんな方法があるようですが, Video for Windows が楽そうだったので それを使うことにしました.

画面の取得

キーワードは capCreateCaptureWindow です.

というわけで, これを使って簡単なアプリを作る手順は

  1. 新規ダイアログアプリのプロジェクトを作る (便宜上 Hoge という名前にしておきます).
  2. 真ん中に IDC_CAPWINDOW という名で Picture Control を作る 大きさは 180x160 にしておく
  3. HogeDlg.cpp ファイルに対し, 頭に以下の二行を追加
    #include <vfw.h>
    #pragma comment(lib,"vfw32.lib")
  4. HogeDlg.h にて HogeDlg の宣言内に以下のフレーズを追加
    HWND m_hWndCap;
  5. HogeDlg::OnInitDialog() にて以下のフレーズを追加
    CWnd* pWnd = GetDlgItem(IDC_CAPWINDOW);
    m_hWndCap = capCreateCaptureWindow(
      "Captrue Window",
      WS_CHILD | WS_VISIBLE,
      0, 0,
      320, 240,
      pWnd->m_hWnd,
      0);
    capDriverConnect(m_hWndCap, 0);
    capPreviewRate(m_hWndCap, 1);
    capPreview(m_hWndCap, TRUE);
    
といった感じでしょうか. 細かい説明はしませんので, 解像度その他を変えたい場合は適当に意味を調べて変えてください.

画像の保存

カメラ画像をファイルに保存する方法ですが, capFileSaveDIB を使えばできます. 具体的には例えば, 適当なボタンを作りそのボタンが押されたら 呼ばれる関数を作ります (HogeDlg::OnBnClickedTest). その中で

capFileSaveDIB(m_hWndCap, "hoge.bmp");
とでも書くと保存されます.

画像の加工 1

得た画像に対して何か処理して表示させたいことがあります. 一番簡単な方法は保存したデータを読み込んで加工して表示する ことです.

まず, 最初に保存したデータを表示する方法ですが,

  1. ダイアログ上で IDC_CAPWINDOW をコピーして横に並べ IDC_DRAW と名づける.
  2. IDC_DRAW に対し m_Draw とでもいう名のコントロール変数をつける.
  3. HogeDlg.h にて HogeDlg の宣言内に以下のフレーズを追加
    CDC *m_pDC;
  4. HogeDlg::OnInitDialog() にて以下のフレーズを追加
    m_pDC=m_Draw.GetDC();
  5. 先ほど書いた capFileSaveDIB の次の行から以下のフレーズを追加
    file.Open("hoge.bmp", CFile::modeRead | CFile::typeBinary);
    nBMPSize=file.GetLength();
    BYTE *pDib = (BYTE *)new char[nBMPSize];
    file.Read(pDib, nBMPSize);
    file.Close();

    BITMAPFILEHEADER *pBMFH = (BITMAPFILEHEADER*)pDib;
    BITMAPINFOHEADER *pBMIH = (BITMAPINFOHEADER *)(pDib + sizeof(BITMAPFILEHEADER));
    BITMAPINFO *pBMI = (BITMAPINFO *)pBMIH;
    BYTE *pData = pDib + pBMFH->bfOffBits;

    //// ここ ///

    ::StretchDIBits(m_pDC->GetSafeHdc(),
    0, 0, pBMIH->biWidth, pBMIH->biHeight,
    0, 0, pBMIH->biWidth, pBMIH->biHeight,
    pData, pBMI, DIB_RGB_COLORS, SRCCOPY);

    delete pDib;
てな感じでしょう. なお, エラー処理はまったく入れてませんので必要に応じ入れてください (もっと言えば m_pDC の開放も行っていません).

「ここ」と書いた所に適当な画像処理を記述できます. 例えば, 下から 3 ピクセル目に赤い線を引きたければ

int y=2;
for (int x=0;x<320;x++){ int Offset=(y*320+x)*3; pData[Offset+2]=0xff; pData[Offset]=pData[Offset+1]=0; }
とでも書けばいいでしょう (ただし, これは私が遊んでいるカメラの場合で, 他のカメラでは 違うかもしれません).

なお, ここでは単純のため, ボタンを押したら処理を行っていますが 例えば監視プログラムのように一定間隔で処理をしたいなら SetTimer という関数を使えばいいと思います.

また, 加工した画像を保存したい場合は delete 文の前あたりで次のように書くとよろしいようです.

FILE *fp=fopen("auau.bmp", "wb");
fwrite (pDib, pBMFH->bfSize, 1, fp);
fclose(fp);

画像の加工 2

上記方法は一応動くんで遊んでみる分には申し分ないんですが, やはり毎回一旦ファイルに落として読むため, 精神衛生上 好ましくありません. できたらファイルを介さずメモリ上で完結させたいものです.

そういう方法を最近見つけましたので書いておきます (と言うか見つけたのでこのページを書くことにした). ただし, あまり美しくありませんが...

  1. HogeDlg.cpp の頭の方に以下のフレーズを追加
    BYTE *g_pBMI;
    volatile bool g_bGetting;
    
    LRESULT PASCAL FrameCallbackProc(HWND hWndCap, LPVIDEOHDR lpVHdr) {
     if (!g_bGetting){
      g_bGetting=true;
    
      BITMAPINFOHEADER nFormat;
      capGetVideoFormat(hWndCap, &nFormat, sizeof(BITMAPINFOHEADER));
    		
      int nBMPSize=nFormat.biSizeImage + sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER);
    
      if (g_pBMI) {
       BITMAPFILEHEADER *pBMFH = (BITMAPFILEHEADER*)g_pBMI;
       if (pBMFH->bfSize != nBMPSize){
        delete [] g_pBMI;
        g_pBMI=NULL;
       }
      }
      if (!g_pBMI){
       g_pBMI = (BYTE *)new char[nBMPSize];
       BITMAPFILEHEADER *pBMFH = (BITMAPFILEHEADER*)g_pBMI;
       pBMFH->bfType = *((WORD*)"BM");
       pBMFH->bfSize = nBMPSize;
       pBMFH->bfReserved1 = pBMFH->bfReserved2 = 0;
       pBMFH->bfOffBits = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER);
      }
      BITMAPINFOHEADER *pBMIH = (BITMAPINFOHEADER *)(g_pBMI + sizeof(BITMAPFILEHEADER));
      memcpy(pBMIH, &nFormat, sizeof(BITMAPINFOHEADER));
      memcpy(g_pBMI + sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER), lpVHdr->lpData, lpVHdr->dwBufferLength);
    
      g_bGetting=false;
     }
     return TRUE ;
    }
    
    BYTE *CapDIB(){
     while (g_bGetting);
     g_bGetting=true;
    
     BITMAPFILEHEADER *pBMFH = (BITMAPFILEHEADER*)g_pBMI;
     BYTE *pRet = (BYTE *)new char[pBMFH->bfSize];
     memcpy(pRet, g_pBMI, pBMFH->bfSize);
     g_bGetting=false;
     return pRet;
    }
    
  2. HogeDlg::OnInitDialog() の最後の方で以下のフレーズを追加
    g_pBMI=NULL;
    g_bGetting=false;
    capSetCallbackOnFrame(m_hWndCap, FrameCallbackProc);
  3. HogeDlg::OnBnClickedTest にて capFileSaveDIB 〜 file.Close の代わりに以下のように書く
    BYTE *pDib = CapDIB();
なお, 書くのが面倒なのでエラー処理は含めてません. デバックモードで動かしててメモリリークの表示が気になる方は 適当な所 (DestroyWindow 等)で m_pBMI を delete すればいいでしょう.

この方法も不必要にメモリコピーを繰り返しているわけで, 不効率と言えば不効率です. ファイルのセーブロードとどっちが いいかは悩みどころですが...


2005.9