Visual Studio (VC++) 応用編 (その6)
− 画像のフレーム間差分による動領域の抽出 −
信州大学工学部 井澤裕司
(H18.2.22)
課 題 6
この課題では,「 静止したカメラ 」を用いて撮影した連続画像を対象に,その「 フレーム間差分 」から「 動領域 」を検出し,「 対象物の動き量 」を計測する手法について,プログラミング実習を行います.
次の図に示すように,「 静止したカメラ 」を用いて,「 静止した背景 」の手前で「 移動する対象物 」を連続的に撮影します.
この図の場合,背景は「 バス停 」で,対象物は「 走る人 」になります.
連続する画像を,「 フレーム N 」と「 フレーム N+1 」として,対応する画素の輝度信号を求め,その「 差分の絶対値 」をとります.
この値が,ある閾値を越えたときは「 グレイ 」,越えないときは「 白 」で表現すると,下の図のようになります.
このグレイの部分が「 動領域 」であり,その画素数が多ければ多いほど,対象物の「 動き量は大きい 」と判定できます.
逆に,「 静止した背景 」は差分が 0 となり,「 白 」の領域で表わされます.
このような考え方で,どの程度正確に「 対象物の動き 」が計測できるか,実際にプログラムを作成して評価します.
それでは,具体的なプログラミングの手順について,説明しましょう.
2.主な手順
「 USBカメラ 」 の画像をキャプチャして 「 画面上に表示 」 する手法については,十分習熟されたと思います.
本編でも, 「 詳細な手順 」 は省略し,フレーム間差分から動き量を計測する手法のプログラムの内容を重点的に説明します.
主な 「 手順 」 は,以下の通りです.
(1) リソースの 「 メニュー 」 の追加「 Visual C++ 」 の操作法について, 不明な点が残っているのであれば,再度 「 応用編の前半 」 に戻って復習して下さい.
(2) リソースの 「 ビットマップ 」 の追加
(3) ソースコード 「 Cmotion_estView() 」の追加・修正
(4) 「 ライブラリ (vfw32.lib) 」 のリンク設定
(5) 「 ビルド 」 の実行とデバッグ
(6) 完成した 「 プロジェクト 」 の実行
3.メニューの追加
これまでのように,新しいプロジェクトとして 「 motion_est 」 を生成し, 以下の表に示すように,「 新規メニュー 」 を追加して下さい.
次に,「 イベントハンドラ 」 を用いて,「 motion_estView() 」 の中に関数を追加します
メニュー サブメニュー ID 関数名 初期設定 解像度 ID_INIT OnInit() 画像解析 動き量 ID_MOTION OnMotion
4.ビットマップの追加
次の表のように,新しく 「 ビットマップ 」 を作成します.
ID 位置 (Height) 位置 (Width)
Colors IDB_BITMAP1 240 320 Trueカラー
5.ソースコードの追加・修正
Viewクラス 「 Cmotion_estView() 」 の最初に,ヘッダーファイルをインクルードし, 画像表示を行うための 「 変数群 」 を追加します.
‥(略)‥
#include "motion_estView.h"
#include "vfw.h" // 追加したヘッダーファイル
#include "winbase.h" // 追加したヘッダーファイル
#define X_SIZE 320 // 水平画素数
#define Y_SIZE 240 // 垂直画素数
#define COLOR 4 // 色(RGB(A) 4 Byte)
#define MAX_TIMES 100 // 動き量の計算回数
#define TH_DIFF 40 // 背景との差分を2値化する閾値
static unsigned char org_img[Y_SIZE][X_SIZE][COLOR]; // 現フレーム画像
static unsigned char pre_img[Y_SIZE][X_SIZE][COLOR]; // 前フレーム画像
static unsigned char mov_img[Y_SIZE][X_SIZE][COLOR]; // 動領域
int motion[MAX_TIMES]; // 動き量
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]
デストラクタ「 Cmotion_estView::~Cmotion_estView() 」 の内部に,後処理用(リソースの解放)のコードを追加します.
Cmotion_estView::~Cmotion_estView()
{
capDriverDisconnect( m_hCapWnd );
::DestroyWindow( m_hCapWnd );
m_lpData = NULL;
free( m_lpImage );
m_lpImage = NULL;
DeleteDC( m_memDC );
DeleteObject( m_hBitmap );
以下にその編集画面を示します.
5-[c]
デストラクタ「 Cmotion_estView::~Cmotion_estView() 」 の後に,画像キャプチャ終了時に起動するコードを追加します.
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]
「 Cmotion_estView::PreCreateWindow(CREATESTRUCT& cs) 」 の内部に,描画準備用のコードを追加します.
BOOL Cimage_fileView::PreCreateWindow(CREARESTRCT& cs)
{
int i;
dispFlag = FALSE;
m_hCapWnd = capCreateCaptureWindowA("motion_est",
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 );
for( i = 0; i < 5; i++ ){
capGrabFrame( m_hCapWnd ); // 立ち上げ時のキャプチャ命令無応答の一時的対策
}
return CView::PreCreateWindow(cs);
}
以下にその編集画面を示します.
5-[e]
・「 OnDraw() 」 の引数のコメントを除去し,次のように修正します.
void Cmotion_estView::OnDraw( CDC* pDC )
・次に,以下に示す描画用のコードを追加します.
CDC pM;
CBitmap pB, *pOld;
// 前フレーム画像の表示
pB.LoadBitmap( IDB_BITMAP1 );
pM.CreateCompatibleDC( pDC );
SetBitmapBits( pB, X_SIZE * Y_SIZE * COLOR, &pre_img); // 前フレームのセット
pOld = pM.SelectObject ( &pB );
pDC->BitBlt (50, 50, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY); // 画面左上に表示
// 現フレーム画像の表示
SetBitmapBits( pB, X_SIZE * Y_SIZE * COLOR, &org_img); // 現フレームのセット
pOld = pM.SelectObject ( &pB );
pDC->BitBlt (400, 50, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY); // 画面右上に表示
// 動領域の表示
SetBitmapBits( pB, X_SIZE * Y_SIZE * COLOR, &mov_img); // 動領域のセット
pOld = pM.SelectObject ( &pB );
pDC->BitBlt (400, 350, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY); // 画面左下に表示
pDC->TextOutW(170, 20, (CString)"前フレーム"); // 前フレームの表示
pDC->TextOutW(520, 20, (CString)"現フレーム"); // 現フレームの表示
pDC->TextOutW(530, 320, (CString)"動領域"); // 動領域の表示
pDC->TextOutW( 30, 320, (CString)"動き量"); // 動き量の表示
pDC->TextOutW(290, 600, (CString)"フレーム(時間)"); // フレームの表示
5-[f]
メニューの「解像度」に対応する「 OnInit() 」 の内部に,以下に示す描画用のコードを追加します.
void Cmotion_estView::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]
「 OnMotion() 」 で使用する以下の関数(整数の絶対値)を追加します.
unsigned char iabs( int a)
{
int abs;
if ( a >= 0 ) abs = a;
else abs = -a;
if ( abs > 255 ) return 255;
else return (unsigned char) abs;
}
以下にその編集画面を示します.
5-[h]
自動生成された「 OnMotion() 」 の内部に,以下に示す描画用のコードを追加します.
CClientDC dc(this);
CDC pM;
CBitmap pB, *pOld;
int it;
int ix, iy;
BYTE *m_lpData2;
unsigned char r, g, b;
unsigned char y; // 輝度信号
int sum; // 差分の絶対値総和
char buf[30]; // 文章表示用のバッファ
CPen redPen(PS_SOLID, 1, RGB(255, 0, 0)); // 赤色ペンの設定
CPen grayPen(PS_SOLID, 1, RGB(192, 192, 192)); // 灰色ペンの設定
CPen* pOldPen; // ペンの保存
pM.CreateCompatibleDC( &dc );
pB.LoadBitmap( IDB_BITMAP1 );
dc.FillSolidRect(50, 320, 500, 300, RGB(255, 255, 255)); // 表示画面のクリア
pOldPen = dc.SelectObject(&grayPen); // 灰色を設定
dc.MoveTo( 50, 590); // グラフ表示の水平座標軸
dc.LineTo(380, 590);
dc.MoveTo( 50, 390); // グラフ表示の垂直座標軸
dc.LineTo( 50, 590);
dc.TextOutW( 45, 602, (CString)"0"); // 原点"0"の表示
dc.TextOutW(170, 20, (CString)"前フレーム"); // 前フレームの表示
dc.TextOutW(520, 20, (CString)"現フレーム"); // 現フレームの表示
dc.TextOutW(530, 320, (CString)"動領域"); // 動領域の表示
dc.TextOutW( 30, 320, (CString)"動き量"); // 動き量の表示
dc.TextOutW(290, 600, (CString)"フレーム(時間)"); // フレームの表示
// フレーム間差分による動き量の計測を繰り返す
for( it = 0; it < MAX_TIMES; it++){
// 画像のキャプチャ
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++){
pre_img[iy][ix][0] = org_img[iy][ix][0]; // 現フレーム ⇒ 前フレーム
pre_img[iy][ix][1] = org_img[iy][ix][1];
pre_img[iy][ix][2] = org_img[iy][ix][2];
}
}
// 現フレームの生成
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信号 (輝度)
org_img[Y_SIZE-iy-1][ix][0] = y; // データは上下反転している
org_img[Y_SIZE-iy-1][ix][1] = y; // このため上下を入れ替える
org_img[Y_SIZE-iy-1][ix][2] = y; // モノクローム画像
}
}
// 動領域の生成
sum = 0; // 差分のクリア
for( iy = 0; iy < Y_SIZE; iy++){
for( ix = 0; ix < X_SIZE; ix++){
if ( iabs( pre_img[iy][ix][0] - org_img[iy][ix][0]) > TH_DIFF ){
mov_img[iy][ix][0] = 255; // 白画素
mov_img[iy][ix][1] = 255;
mov_img[iy][ix][2] = 255;
sum++;
} else {
mov_img[iy][ix][0] = 0; // 黒画素
mov_img[iy][ix][1] = 0;
mov_img[iy][ix][2] = 0;
}
}
}
motion[it] = sum;
// 前フレームの表示
SetBitmapBits( pB, X_SIZE * Y_SIZE * COLOR, &pre_img); // 配列のセット
pOld = pM.SelectObject(&pB);
dc.BitBlt( 50, 50, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY); // 画像の表示 (画面左上)
pM.SelectObject( pOld );
// 現フレームの表示
SetBitmapBits( pB, X_SIZE * Y_SIZE * COLOR, &org_img); // 配列のセット
pOld = pM.SelectObject(&pB);
dc.BitBlt( 400, 50, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY); // 画像の表示 (画面右上)
pM.SelectObject( pOld );
// 動領域の表示
SetBitmapBits( pB, X_SIZE * Y_SIZE * COLOR, &mov_img); // 配列のセット
pOld = pM.SelectObject(&pB);
dc.BitBlt( 400, 350, X_SIZE, Y_SIZE, &pM, 0, 0, SRCCOPY); // 画像の表示 (画面右下)
pM.SelectObject( pOld );
// 動き量をグラフ表示
pOldPen = dc.SelectObject(&redPen); // 赤色を設定
if ( it == 0 ) dc.MoveTo(50, 590);
else dc.LineTo( 50 + ( it - 1) * 3, 590 - motion[it] / 300); // 赤の折れ線グラフを描画
// フレームを数値で表示
sprintf( buf, "フレーム = %6d ", it); // フレーム数の表示
dc.TextOutW(160, 340, (CString) buf);
// 動き量を数値で表示
sprintf( buf, "動き量 = %6d ", (int)(motion[it] / 300)); // 動き量の数値表示
dc.TextOutW(160, 360, (CString) buf);
}
以下にその編集画面を示します.(画像サイズが大きいため,上下2つに分けています).
6.ライブラリ[vfw32.lib] のリンク設定
メニュー「 表示 」の「 プロパティマネージャ 」をクリックして,「 構成プロパティ 」を開き,「 リンカ 」,「 入力 」を選択します.
右側の「 追加の依存ファイル 」の欄に「 vfw32.lib 」と入力します.
7.ビルドとデバッグ
「 ビルド 」を実行して,エラーメッセージがなくなるまで,「 デバッグ 」を繰り返します.
8.プロジェクトの実行
「 実行結果 」の例を以下に示します.
画面の下に,「 対象物の動き量 」が「 折れ線グラフ 」で表示されています.
9.まとめ
本章では,「 フレーム間差分 」を用いて「 対象物の動き量 」を計測するプログラムを作成しました.
このプログラムを用いて,「 様々な背景 」のもとで「 対象物 」を動かしながら,その「 計測結果 」を評価して下さい.
例えば,上の実行例の場合,「 背景部 」は問題なく除去されていますが,「 マウスの内部 」には,「 黒いエリア 」すなわち,「 動いていない領域 」と判定されている部分が含まれています.
これを除去するには,例えば「 青い背景 」を用いて,対象物の領域を正確に抽出する「 クロマキー処理 」などが必要になります.
ただし,「 対象物 」の中に「 青いエリア 」があると,「 背景の一部 」とみなされてしまいます.
一方,「 対象物 」の「 動きが速く 」なった場合の計測について,以下の図を用いて考えてみましょう.
動きが大きいときは,図のようにフレーム間で,「 重なった領域 」がなくなってしまます.
このとき,「 動き量 」は,2つの「 対象物の面積の和 」で表されるので,「 対象物の速度 」に対応しなくなってしまいます.
また,「 動きが速く 」なったとき,カメラの画像の「 ブレやボケ 」が計測結果に,悪い影響を与えてしまいます.
このように,「 フレーム間差分 」を用いた「 対象物の動き量 」の計測手法には,いくつかの問題点が残されていますが,「 大雑把な交通量の計測 」や「 人間や動物の活動量 」の計測に,利用できる可能があります.