Visual Studio (VC++) 応用編 (その4)
− 画像のテンプレートマッチングによる類似領域の抽出 −
信州大学工学部 井澤裕司
(H18.2.22)
課 題 4
Microsoft Visual Studio2005 の Visual C++ を用いて,以下の機能をもつプログラムを作成しなさい.
MFCアプリケーションのメニューの「
初期設定
」,サブメニューの「
解像度
」 を起動すると,接続したUSBカメラの画素数等の情報を設定するダイヤログが開くので,解像度を「
320×240
」,ピクセルビットを「
RGB24
」に設定する.次に,メニューの「
初期設定
」,サブメミューの「
テンプレート画像
」を指定すると,USBカメラでキャプチャしたフルカラーの静止画像が解像度「
320×240画素
」で画面の左側に表示される. |
|
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.ソースコードの追加・修正
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) 「 照明条件 」 の問題
蛍光灯を使用したときのように「 照度 」 が「 時間的に変化 」している場合も,(3) 「 カメラやレンズ 」の問題
2枚の画像の相違度に誤差が生じ,正しく抽出できないことがあります.
「 カメラの雑音 」 や「 動きによるボケ 」 があると,抽出精度は極端に低下します.このように, 「 カメラの画像信号 」 を用いて「 計測 」 や「 解析 」 を行う場合,その「 使用環境 」 を明確にすることが極めて重要であり,
同様に,「 レンズ 」の「 コマ収差 」や「 ボケ 」なども精度低下の要因となります.
それらを 「 定量的 」に抑える必要があります.
「 信号の解析手法 」 にどのようなものを選ぼうとも,それらの土台がなければ,無力であることを理解して下さい.
来月は,「 物体の移動速度検出手法 」について実習を行う予定です.