背景統計量の累積
■ 背景と注目物体の分離
OpenCVには,背景差分を計算する際に便利な背景統計量の累積に関する関数が実装されている.画像中から,変化のない背景領域とそれ以外の領域を分離することは,コンピュータビジョンシステムにおいて多く使用される技術であり,様々な手法が提案されている. OpenCVにおいても,cvauxで2種類の動的背景差分手法が実装されている. ここでは,参考文献[1]で用いられている背景画像の時間的な変化を考慮した動的背景更新によるロバストな注目物体の検出手法を,OpenCVの関数を用いて実装した.
この手法では,背景領域画素の輝度 I を以下のようにモデル化している.
このモデルにおいて, I が, \overline{I} - \sigma - \zeta \leq I \leq \overline{I} + \sigma + \zeta の場合に,その画素は背景領域に存在する画素であると判断する.
背景と判定された領域では輝度平均値 \overline{I}と振幅 \sigma を以下の式を用いて更新する.
一方,物体領域と判定された領域では,輝度平均値は元の値を保持し,振幅 \sigma のみを以下の式を 用いて更新する.
[1] 森田 真司, 山澤 一誠, 寺沢 征彦, 横矢 直和: "全方位画像センサを用いたネットワーク対応型遠隔監視システム", 電子情報通信学会論文誌(D-II), Vol. J88-D-II, No. 5, pp. 864-875, (2005.5).
サンプル
動的背景更新による物体検出 cvAcc, cvRunningAvg
背景画像の時間的な明度変化を考慮した物体検出.プログラム開始より,ウィンドウが表示されるまでの間は背景統計量を初期化しているためカメラを動かさないように注意する.サンプル内の初期化フレーム数や更新パラメータはテストで使用カメラに関してチューニングされている.また,OpenCV関数の使用例提示を優先したため,処理効率は若干低い.
サンプルコード
#include <cv.h> #include <highgui.h> #include <ctype.h> #include <stdio.h> int main (int argc, char **argv) { int i, c, counter; int INIT_TIME = 100; int w = 0, h = 0; double B_PARAM = 1.0 / 50.0; double T_PARAM = 1.0 / 200.0; double Zeta = 10.0; CvCapture *capture = 0; IplImage *frame = 0; IplImage *av_img, *sgm_img; IplImage *lower_img, *upper_img, *tmp_img; IplImage *dst_img, *msk_img; CvFont font; char str[64]; // (1)コマンド引数によって指定された番号のカメラに対するキャプチャ構造体を作成する if (argc == 1 || (argc == 2 && strlen (argv[1]) == 1 && isdigit (argv[1][0]))) capture = cvCreateCameraCapture (argc == 2 ? argv[1][0] - '0' : 0); // (2)1フレームキャプチャし,キャプチャサイズを取得する. frame = cvQueryFrame (capture); w = frame->width; h = frame->height; // (3)作業用の領域を生成する av_img = cvCreateImage (cvSize (w, h), IPL_DEPTH_32F, 3); sgm_img = cvCreateImage (cvSize (w, h), IPL_DEPTH_32F, 3); tmp_img = cvCreateImage (cvSize (w, h), IPL_DEPTH_32F, 3); lower_img = cvCreateImage (cvSize (w, h), IPL_DEPTH_32F, 3); upper_img = cvCreateImage (cvSize (w, h), IPL_DEPTH_32F, 3); dst_img = cvCreateImage (cvSize (w, h), IPL_DEPTH_8U, 3); msk_img = cvCreateImage (cvSize (w, h), IPL_DEPTH_8U, 1); // (4)背景の輝度平均の初期値を計算する printf ("Background statistics initialization start\n"); cvSetZero (av_img); for (i = 0; i < INIT_TIME; i++) { frame = cvQueryFrame (capture); cvAcc (frame, av_img); } cvConvertScale (av_img, av_img, 1.0 / INIT_TIME); // (5)背景の輝度振幅の初期値を計算する cvSetZero (sgm_img); for (i = 0; i < INIT_TIME; i++) { frame = cvQueryFrame (capture); cvConvert (frame, tmp_img); cvSub (tmp_img, av_img, tmp_img); cvPow (tmp_img, tmp_img, 2.0); cvConvertScale (tmp_img, tmp_img, 2.0); cvPow (tmp_img, tmp_img, 0.5); cvAcc (tmp_img, sgm_img); } cvConvertScale (sgm_img, sgm_img, 1.0 / INIT_TIME); printf ("Background statistics initialization finish\n"); // (6)表示用ウィンドウを生成する cvInitFont (&font, CV_FONT_HERSHEY_COMPLEX, 0.7, 0.7); cvNamedWindow ("Input", CV_WINDOW_AUTOSIZE); cvNamedWindow ("Substraction", CV_WINDOW_AUTOSIZE); // (7)取得画像から背景を分離するループ counter = 0; while (1) { frame = cvQueryFrame (capture); cvConvert (frame, tmp_img); // (8)背景となりうる画素の輝度値の範囲をチェックする cvSub (av_img, sgm_img, lower_img); cvSubS (lower_img, cvScalarAll (Zeta), lower_img); cvAdd (av_img, sgm_img, upper_img); cvAddS (upper_img, cvScalarAll (Zeta), upper_img); cvInRange (tmp_img, lower_img, upper_img, msk_img); // (9)輝度振幅を再計算する cvSub (tmp_img, av_img, tmp_img); cvPow (tmp_img, tmp_img, 2.0); cvConvertScale (tmp_img, tmp_img, 2.0); cvPow (tmp_img, tmp_img, 0.5); // (10)背景と判断された領域の背景の輝度平均と輝度振幅を更新する cvRunningAvg (frame, av_img, B_PARAM, msk_img); cvRunningAvg (tmp_img, sgm_img, B_PARAM, msk_img); // (11)物体領域と判断された領域では輝度振幅のみを(背景領域よりも遅い速度で)更新する cvNot (msk_img, msk_img); cvRunningAvg (tmp_img, sgm_img, T_PARAM, msk_img); // (12)物体領域のみを出力画像にコピーする(背景領域は黒) cvSetZero (dst_img); cvCopy (frame, dst_img, msk_img); // (13)処理結果を表示する snprintf (str, 64, "%03d[frame]", counter); cvPutText (dst_img, str, cvPoint (10, 20), &font, CV_RGB (0, 255, 100)); cvShowImage ("Input", frame); cvShowImage ("Substraction", dst_img); counter++; c = cvWaitKey (10); if (c == '\x1b') break; } cvDestroyWindow ("Input"); cvDestroyWindow ("Substraction"); cvReleaseImage (&frame); cvReleaseImage (&dst_img); cvReleaseImage (&av_img); cvReleaseImage (&sgm_img); cvReleaseImage (&lower_img); cvReleaseImage (&upper_img); cvReleaseImage (&tmp_img); cvReleaseImage (&msk_img); return 0; }
// (1)コマンド引数によって指定された番号のカメラに対するキャプチャ構造体を作成する
関数 cvCreateCameraCapture()を用いて,
コマンド引数として与えられた番号のカメラに対するキャプチャ構造体 CvCaptureを作成する.
引数が指定されない場合は,デフォルトの値である"0"が利用される.
// (2)1フレームキャプチャし,キャプチャサイズを取得する.
関数 cvQueryFrame() を呼び出し,実際に1フレームキャプチャを行い,
取得した画像から,画像の幅wと高さhを取得する.
// (3) 作業用の領域を生成する
輝度平均や輝度振幅計算の為の作業領域を確保する.累積計算や累乗計算を行なうため,
デプスは32ビットを指定しておく.マスク画像は8ビット,シングルチャンネルを指定する.
// (4) 背景の輝度平均の初期値を計算する
背景のモデル化の為に,指定されたフレーム数(INIT_TIME)間の各画素値の平均を求める.
cvAcc()関数を用いて,画像の累積値を計算する.
// (5) 背景の輝度振幅の初期値を計算する
(4)で計算された背景画像の平均値を用いて,輝度振幅の初期値を計算する.
輝度振幅 \sigma は
\sqrt{2 \times ( I - \overline{I})^2}として計算する.
輝度振幅についても指定されたフレーム数(INIT_TIME)間の平均を求める.
// (6)表示用ウィンドウを生成する
入力画像,出力画像を表示するウィンドウを生成する.
// (7)取得画像から背景を分離するループ
背景画像モデルの初期値を計算した後,取得画像から背景分離を行なうループに入る.
// (8)背景となりうる画素の輝度値の範囲をチェックする
画像の各画素について
\overline{I} - \sigma - \zeta \leq I \leq \overline{I} + \sigma + \zeta
を計算し,cvInRange()で値が範囲内かどうかを比較し,msk_imgに結果を保存して返す.
範囲内の画素に関してはmsk_imgの対応する画素値が0xFFとしてセットされる.
// (9)輝度振幅を再計算する
(5)と同様にして各画素の輝度振幅 \sigma を再計算する.
// (10)背景と判断された領域の背景の輝度平均と輝度振幅を更新する
(8)で返されるマスク画像を用いて,背景領域のみ cvRunningAvg()関数を用いて輝度平均と輝度振幅を更新する.
// (11)物体領域と判断された領域では輝度振幅のみを(背景領域よりも遅い速度で)更新する
物体領域の輝度振幅を更新するために,マスク画像を反転させる.その後(10)と同様にして輝度振幅を更新する.
ここで使用する更新速度パラメータは背景領域更新に使用するパラメータよりも大きくなければならない.
// (12)物体領域のみを出力画像にコピーする(背景領域は黒)
処理結果を示すために,画像dst_imgに対して,frameからマスクにしたがって画素値をコピーする.
// (13)処理結果を表示する
入力画像と,処理結果を表示する.キャプチャ中に"Esc"キーが押された場合は,終了する.