Visual Studio (VC++) 応用編 (その4)


− 画像のテンプレートマッチングによる類似領域の抽出 −


信州大学工学部 井澤裕司

(H18.2.22)


課  題 4

Microsoft Visual Studio2005 の Visual C++ を用いて,以下の機能をもつプログラムを作成しなさい.

MFCアプリケーションのメニューの「 初期設定 」,サブメニューの「 解像度 」 を起動すると,接続したUSBカメラの画素数等の情報を設定するダイヤログが開くので,解像度を「 320×240 」,ピクセルビットを「 RGB24 」に設定する.次に,メニューの「 初期設定 」,サブメミューの「 テンプレート画像 」を指定すると,USBカメラでキャプチャしたフルカラーの静止画像が解像度「 320×240画素 」で画面の左側に表示される.
(以上は,応用編の課題1と同じ)
なお,この画像の中央にある「 赤色の枠 」がテンプレート領域であり,サイズは「 32×32画素 」である.

次に,メニューの「 画像解析 」,サブメニューの「 テンプレートマッチング 」 を起動すると,USBカメラの画像信号がキャプチャされ,画面右側にモノクローム画像として表示される.このとき,「 赤い枠 」で表わされる領域は,左側の「 テンプレート画像 」に最も「 類似度 」の高い領域となる.

この課題では,評価関数として「 類似度 」と逆の関係にある「 相違度 」を用いるものとする.これは,「 テンプレート画像 」と同じ大きさの「 画像領域 」について,「 差分の絶対値総和 」をとったものである. 画面全体について,この「 相違度 」が最小となるエリアを探索する.画面の下には,「 相違度の最小値 」と「 水平・垂直方向の位置(ズレ) 」を表示する.


[注意]
このアプリケーションは,Video for Windows(以下,VFWと呼ぶ)のライブラリを使用しています. USBカメラに限らず,PCIインタフェースのキャプチャーカードを用いても動作しますが, 事前に使用するUSBカメラ,もしくはキャプチャーカードのデバイスドライバをインストールし,正常に動作させる必要があります.
なお,このコンテンツで紹介するプログラムは,研究室における試行錯誤から生まれたものであり, VFWの正規の使用法に準拠している保証はありません.操作方法により,動作が不安定になる場合があることをご了解下さい.


このコンテンツの中で,Visual C++ の使用法を詳細に説明することは困難です. Visual C++については,以下のような書籍が出版されていますので,参考にして下さい.
   ・Visual C++ 2005 ビギナー編
        林 晴彦 Softbank Creative
 
WEB上には 以下のようなサイトがあります.
 
   ・[Visual C++の使い方]
          http://www.nitoyon.com/vc/
   ・[Visual C++の勉強部屋]
          http://homepage3.nifty.com/ishidate/vcpp.htmvc/
   ・[Area of VC++ Tips]
          http://rararahp.cool.ne.jp/vc/vctips/tipindex.htm
 

1.はじめに

本章では「 テンプレートマッチング 」 を用いて,画像の中から 「 類似するエリアを抽出 」 する手法について,プログラム実習を行います.

テンプレート 」 とは,一般に「 型紙 」のことを表します.
USBカメラを用いて撮影した1枚のモノクローム画像があるとします.
例えば,下に示す画像 (320×240画素) を用いて説明しましょう.
中央に「 赤い枠 」で囲ったエリアがありますが,これが「 32×32画素 」 の「 テンプレート画像 」 です.



USBカメラ 」 の向きを変えて撮影したもう1枚の画像を下に示します.
この画像を探索して,上に示した 「 テンプレート 」 の画像に最も 「 類似しているエリア 」 を抽出します.
赤色の枠 」 をで囲った部分が,その結果です.
この例のように 「 静止した対象物 」 を撮影した時,画面上の「 位置のズレ 」 を計測すれば,カメラの動きを定量化することができます.




この課題では,評価関数として「 類似度 」と逆の関係にある「 相違度 」を用います.

この指標は,「 テンプレート画像 」と同じ大きさの「 画像の領域 」について,「 差分の絶対値総和 」をとったものです.
画面全体について,この「 相違度 」が最小となるエリアを探索します.
探索終了後,画面の下には,「 相違度の最小値 」と「 水平・垂直方向の位置(ズレ) 」を表示します.

また「 一定の時間間隔 」で連続して撮影した画像を用いれば,「 水平・垂直方向のズレ 」は,「 USBカメラ 」の「 連続的な動き 」を表すことになります.
しかし,それらが成立するためには「 対象物やカメラの動き 」 についての一定の制約条件が必要です.

この課題ではそれらの条件について考察し,「 物体の動き 」を計測する手がかりとします.


2.主な手順

USBカメラ 」 の画像をキャプチャして 「 画面上に表示 」 する手法については,習熟されたと思います.
本編では,これまでのような 「 詳細な手順 」 は省略し,テンプレートマッチングのコーディングを重点的に説明することにします.

主な 「 手順 」 は,以下の通りです.

(1)  リソースの 「 メニュー 」 の追加

(2)  リソースの 「 ビットマップ 」 の追加

(3)  ソースコード 「 Cimg_tmpltmatchView() 」の追加・修正

(4)  ライブラリ (vfw32.lib) 」 のリンク設定

(5)  ビルド 」 の実行とデバッグ

(6)  完成した 「 プロジェクト 」 の実行

Visual C++ 」 の操作法について, 不明な点が残っているのであれば,再度 「 応用編(その1) 」 に戻って,復習して下さい.

3.メニューの追加

これまでのように,新しいプロジェクトとして 「 img_tmpltmatch 」 を生成し, 以下の表に示すように,「 新規メニュー 」 を追加して下さい.
次に,「 イベントハンドラ 」 を用いて,「 img_tmpltmatchView() 」 の中に関数を追加します

メニュー サブメニュー ID 関数名
初期設定 解像度 ID_INIT OnInit()
テンプレート ID_TMPLT_IMAGE OnTmpltImage
画像解析 テンプレートマッチング ID_TMPLT_MATCH OnTmpltMatch


4.ビットマップの追加

次の表のように,新しく 「 ビットマップ 」 を作成します.

ID 位置 (Height) 位置 (Width)
Colors
IDB_BITMAP1 240 320 Trueカラー

5.ソースコードの追加・修正

5-[a]
Viewクラス 「 Cimg_tmpltmatchView() 」 の最初に,ヘッダーファイルをインクルードし, 画像表示を行うための 「 変数群 」  を追加します.



‥(略)‥
#include   "img_tmpltmatchView.h"

#include   "vfw.h"        // 追加したヘッダーファイル
#include   "winbase.h"     // 追加したヘッダーファイル

#define  X_SIZE     320         // 水平画素数
#define  Y_SIZE     240         // 垂直画素数
#define  TMP_SIZE    32         // テンプレートの画素数 (縦横同数)
#define  COLOR      4        // 色(RGB(A) 4 Byte)

static unsigned char ref_img[Y_SIZE][X_SIZE][COLOR];     // リファレンス画像 (テンプレート用)
static unsigned char image[Y_SIZE][X_SIZE][COLOR];      // キャプチャ画像
static unsigned char tmp_img[TMP_SIZE][TMP__SIZE];      // テンプレート画像 (モノクローム)

CDC              m_memDC;          // メモリDC
HBITMAP          m_hBitmap = NULL;     // DIB Section
BITMAPINFOHEADER*  m_lpImage = NULL;
static BYTE*        m_lpData = NULL;      // RGBデータ
HWND             m_hCapWnd = NULL;
int                size;              // データサイズ
BOOL              dispFlag = FALSE;

BITMAPINFOHEADER*  GetDIBHeader(){      // DIB(Device Independent BMP)のヘッダ
       return m_lpImage;
}

BOOL CreateDIBHeader(int size){
           // DIB(Device Independent BMP)のヘッダ作成
       m_lpImage = (BITMAPINFOHEADER*)malloc(size);
       return (m_lpImage != NULL);
}

以下にその編集画面を示します.


5-[b]

デストラクタ「 Cimg_tmpltmatchView::~Cimg_tmpltmatchView() 」 の内部に,後処理用(リソースの解放)のコードを追加します.

Cimg_tmpltmatchView::~Cimg_tmpltmatchView()
{
   capDriverDisconnect( m_hCapWnd );
   ::DestroyWindow( m_hCapWnd );
   m_lpData = NULL;
   free( m_lpImage );
   m_lpImage = NULL;
   DeleteDC( m_memDC );
   DeleteObject( m_hBitmap );


以下にその編集画面を示します.



5-[c]

デストラクタ「 Cimg_tmpltmatchView::~Cimg_tmpltmatchView() 」 の後に,画像キャプチャ終了時に起動するコードを追加します.

static LRESULT FAR PASCAL CaptureImage(HWND hWnd,LPVIDEOHDR lpVHdr)
{
   memcpy(m_lpData, (LPSTR)lpVHdr->lpData, m_lpImage->biSizeImage);
   dispFlag = TRUE;
   return (LRESULT)TRUE;
}

以下にその編集画面を示します.



5-[d]

Cimg_tmpltmatchView::PreCreateWindow(CREATESTRUCT& cs) 」 の内部に,描画準備用のコードを追加します.

BOOL Cimage_fileView::PreCreateWindow(CREARESTRCT& cs)
{
   dispFlag = FALSE;
   m_hCapWnd = capCreateCaptureWindowA("img_tmpltmatch",
        WS_OVERLAPPEDWINDOW, 0, 0, X_SIZE, Y_SIZE, NULL, NULL);

   capDriverConnect(m_hCapWnd, 0);
   capSetCallbackOnFrame( m_hCapWnd, CaptureImage);
   size = capGetVideoFormatSize(m_hCapWnd);
   CreateDIBHeader(size);
   m_lpImage = GetDIBHeader();
   capGetVideoFormat(m_hCapWnd, m_lpImage, size);
// DIBsectionの確保
   m_hBitmap = CreateDIBSection( m_memDC.GetSafeHdc(),
             (BITMAPINFO*)m_lpImage,
             DIB_RGB_COLORS, (void**)&m_lpData, NULL, 0 );

   return CView::PreCreateWindow(cs);
}

以下にその編集画面を示します.


5-[e]

・「 OnDraw() 」 の引数のコメントを除去し,次のように修正します.

    void Cimg_tmpltmatchView::OnDraw( CDC* pDC )

・次に,以下に示す描画用のコードを追加します.

CDC         pM;
CBitmap          pB, *pOld;
CPen         redPen(PS_SOLID, 1, RGB(255, 0, 0));          // 赤色ペンの設定
CPen*       pOldPen;                            // ペンの保存


pB.LoadBitmap( IDB_BITMAP1 );
pM.CreateCompatibleDC( pDC );
SetBitmapBits( pB, X_SIZE * Y_SIZE * COLOR, &ref_img);      // 配列(ref_img)のセット
pOld = pM.SelectObject ( &pB );
pDC->BitBlt (50, 50, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY); // 画像の表示 (画面左側)
pOldPen = pDC->SelectObject(&redPen);                  // 赤色を設定 (枠の表示)
pDC->MoveTo(50 + X_SIZE / 2 - 15,  50 + Y_SIZE / 2 - 15);     // 最も類似度の高いエリアの表示
pDC->LineTo (50 + X_SIZE / 2 + 16,  50 + Y_SIZE / 2 - 15);     // 水平・垂直の幅(TMP_SIZE)
pDC->LineTo (50 + X_SIZE / 2 + 16,  50 + Y_SIZE / 2 + 16);
pDC->LineTo (50 + X_SIZE / 2 - 15,  50 + Y_SIZE / 2 + 16);
pDC->LineTo (50 + X_SIZE / 2 - 15,  50 + Y_SIZE / 2 - 15);
pM.SelectObject( pOld );

SetBitmapBits(pB, X_SIZE * Y_SIZE * COLOR, &image);        // 配列(image)のセット
pOld = pM.SelectObject(&pB);
pDC->BitBlt(400, 50, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY);     // 画像の表示 (画面右側)
pM.SelectObject( pOld );


5-[f]

メニューの「解像度」に対応する「 OnInit() 」 の内部に,以下に示す描画用のコードを追加します.

void Cimg_tmpltmatchView::OnInit()
{
   capDlgVideoFormat(m_hCapWnd);
   size = capGetVideoFormatSize(m_hCapWnd);
   CreateDIBHeader(size);
   m_lpImage = GetDIBHeader();
   capGetVideoFormat(m_hCapWnd, m_lpImage, size);
   m_hBitmap = CreateDIBSection( m_memDC.GetSafeHdc(),
      (BITMAPINFO*)m_lpImage,
     DIB_RGB_COLORS,
       (void**)&m_lpData, NULL, 0 );
}

以下にその編集画面を示します.



5-[g]

自動生成された「 OnTmpltImage() 」 の内部に,以下に示す描画用のコードを追加します.

int            ix, iy;
BYTE          *m_lpData2;
unsigned char     r, g, b;
unsigned char     y;

dispFlag = FALSE;
capGrabFrame( m_hCapWnd );    // 1フレーム(1枚)を取得するコマンド

while( dispFlag == FALSE) { }    // フレームの読み込みが終了するまで待つ
m_lpData2 = (BYTE*)m_lpData;   // 画像メモリのポインタ

for( iy = 0; iy < Y_SIZE; iy++){
    for( ix = 0; ix < X_SIZE; ix++){
       b = *m_lpData2; m_lpData2++;    // Blue  (青)
       g = *m_lpData2; m_lpData2++;    // Green (緑)
       r = *m_lpData2; m_lpData2++;    // Red   (赤)
       y = (unsigned char)(0.299 * (double)r + 0.587 * (double)g + 0.114 * (double)b);
                                // Y(輝度)信号
       ref_img[ Y_SIZE-iy-1 ][ ix ][ 0 ] = y;  // データは上下が反転している
       ref_img[ Y_SIZE-iy-1 ][ ix ][ 1 ] = y;  // このため上下を入れ替える
       ref_img[ Y_SIZE-iy-1 ][ ix ][ 2 ] = y;  // モノクローム画像
    }
}
for( iy = 0; iy < TMP_SIZE; iy++){           // テンプレートサイズ(縦横;TMP_SIZE)
    for( ix = 0; ix < TMP_SIZE; ix++){
       tmp_img[iy][ix]               // ref_img の中心部をテンプレート画像にセット
           = ref_img[(Y_SIZE - TMP_SIZE) / 2 + iy - 1][(X_SIZE - TMP_SIZE) / 2 + ix - 1][0];
    }
}

InvalidateRect( NULL, FALSE ); // OnDrawを強制的に動作させることにより描画する

以下にその編集画面を示します.


5-[h]

OnTmpltMatch() 」 で使用する以下の関数(整数の絶対値)を追加します.

int iabs( int a)
{
     if ( a >= 0 ) return a;      // 正かゼロならば,その値を返す
   else return -a;          // 負ならば,反転して絶対値を返す
}
以下にその編集画面を示します.


5-[i]

自動生成された「 OnTmpltMatch() 」 の内部に,以下に示す描画用のコードを追加します.



CClientDC     dc(this);
CDC         pM;
CBitmap          pB, *pOld;
int          ix, iy;
int          kx, ky;
BYTE        *m_lpData2;
unsigned char   r, g, b;
unsigned char   y;                          // 輝度信号
int         diff;                         // 差分の絶対値総和
int         diff_min;                      // 差分の絶対値総和の最小値
int         x_min;                       // エリア左上の水平座標
int          y_min;                       // エリア左上の垂直座標
char        buf[30];                      // 文章表示用のバッファ
CPen        redPen(PS_SOLID, 1, RGB(255, 0, 0));    // 赤色ペンの設定
CPen*      pOldPen;                      // ペンの保存

pB.LoadBitmap( IDB_BITMAP1 );
pM.CreateCompatibleDC( &dc );

dispFlag = FALSE;
capGrabFrame( m_hCapWnd );                  // 1フレーム(1枚)を取得するコマンド
while( dispFlag == FALSE) { }                  // フレームの読み込みが終了するまで待つ

m_lpData2 = (BYTE*)m_lpData;                 // 画像メモリのポインタ
for( iy = 0; iy < Y_SIZE; iy++){
   for( ix = 0; ix < X_SIZE; ix++){
      b = *m_lpData2; m_lpData2++;            // Blue  (青)
      g = *m_lpData2; m_lpData2++;            // Green (緑)
      r = *m_lpData2; m_lpData2++;            // Red   (赤)
      y = (unsigned char)(0.299 * (double)r + 0.587 * (double)g + 0.114 * (double)b);
                                    // Y信号 (輝度)
      image[Y_SIZE-iy-1][ix][0] = y;           // データは上下反転している
      image[Y_SIZE-iy-1][ix][1] = y;           // このため上下を入れ替える
      image[Y_SIZE-iy-1][ix][2] = y;           // モノクローム画像
   }
}

diff_min = 10000000;                      // エリア内差分の最小値を初期設定

for( iy = 0; iy < Y_SIZE - TMP_SIZE; iy++){
   for( ix = 0; ix < X_SIZE - TMP_SIZE; ix++){
      diff = 0;                         // 差分のクリア
      for( ky = 0; ky < TMP_SIZE; ky++){
         for( kx = 0; kx < TMP_SIZE; kx++){
            diff = diff + iabs(image[iy + ky][ix + kx][0] - tmp_img[ky][kx]);
                                   // 差分の絶対値総和
         }
      }
      if(diff_min > diff){                  // エリア内差分の絶対値総和を最小値と比較する
         diff_min = diff;                 // 最小値の保存 (更新)
         x_min = ix;                    // テンプレート左上の水平座標を保存
         y_min = iy;                    // テンプレート左上の垂直座標を保存
      }
   }
}

SetBitmapBits(pB, X_SIZE * Y_SIZE * COLOR, &image);         // 配列(image)のセット
pOld = pM.SelectObject(&pB);
dc.BitBlt(400, 50, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY);         // 画像の表示 (画面右側)
pM.SelectObject(pOld);

pOldPen = dc.SelectObject(&redPen);                     // 赤色を設定 (枠の表示)
dc.MoveTo(400 + x_min        , 50 + y_min        );     // 最も類似度の高いエリアの表示
dc.LineTo (400 + x_min + TMP_SIZE, 50 + y_min        );     // 水平・垂直の幅(TMP_SIZE)
dc.LineTo (400 + x_min + TMP_SIZE, 50 + y_min + TMP_SIZE);
dc.LineTo (400 + x_min        , 50 + y_min + TMP_SIZE);
dc.LineTo (400 + x_min        , 50 + y_min        );

dc.FillSolidRect(100, 320, 150, 40, RGB(255, 255, 255));         // 表示画面のクリア

sprintf(buf, "x_delta = %d", x_min - (X_SIZE - TMP_SIZE) / 2 + 1);   // 水平方向のズレ ⇒バッファ
dc.TextOutW(100, 320, (CString)buf);                    // 水平方向のズレ表示
sprintf(buf, "y_delta = %d", y_min - (X_SIZE - TMP_SIZE) / 2 + 1);   //垂直方向のズレ⇒バッファ
dc.TextOutW(100, 340, (CString)buf);                    // 垂直方向のズレ表示
sprintf(buf, "diff = %d", diff_min);                       // 差分の絶対値総和 ⇒バッファ
dc.TextOutW(100, 360, (CString)buf);                 // 差分の表示


以下にその編集画面を示します.(画像サイズが大きいため,上下2つに分けています).




6.ライブラリ[vfw32.lib] のリンク設定

メニュー「 表示 」の「 プロパティマネージャ 」をクリックして,「 構成プロパティ 」を開き,「 リンカ 」,「 入力 」を選択します.
右側の「 追加の依存ファイル 」の欄に「 vfw32.lib 」と入力します.

7.ビルドとデバッグ

ビルド 」を実行して,エラーメッセージがなくなるまで,「 デバッグ 」を繰り返します.

8.プロジェクトの実行

実行結果 」の例を以下に示します.
画面の下に,「 相違度 」と「 位置のズレ 」が表示されています.



9.まとめ

本章では,「 テンプレートマッチング 」を用いた「 類似領域の抽出方法 」について,プログラミング実習を行いました.

ここで作成したプログラムを用いて,「 様々な対象物 」を「 カメラ 」を動かしながらキャプチャし,
その抽出結果を評価してみて下さい.
正しい抽出結果 」が得られる場合と,「 誤動作 」している場合があることが分かるはずです.

誤動作 」の原因として,例えば以下のような項目が挙げられます.


(1)  対象物や背景 」 の問題
対象物や背景に特徴がなく,「 平坦な画像 」 や「 縞模様 」等の場合は,本質的に抽出できません.
(2)  照明条件 」 の問題
蛍光灯を使用したときのように「 照度 」 が「 時間的に変化 」している場合も,
2枚の画像の相違度に誤差が生じ,正しく抽出できないことがあります.
(3)  カメラやレンズ 」の問題
カメラの雑音 」 や「 動きによるボケ 」 があると,抽出精度は極端に低下します.
同様に,「 レンズ 」の「 コマ収差 」や「 ボケ 」なども精度低下の要因となります.

このように, カメラの画像信号 」 を用いて「 計測 」 や「 解析 」 を行う場合,その「 使用環境 」 を明確にすることが極めて重要であり,
それらを 「 定量的 」に抑える必要があります.
信号の解析手法 」 にどのようなものを選ぼうとも,それらの土台がなければ,無力であることを理解して下さい.

来月は,「 物体の移動速度検出手法 」について実習を行う予定です.