Codelogy

[OpenCV] 動的背景更新とオブジェクトの認識

最近、画像処理についての会話をよく聞くので少し自分でもやってみました。
今回使用したライブラリはIntel社のOpenCVというライブラリです。

ダウンロードは以下のリンクより行えます。
http://sourceforge.net/projects/opencvlibrary/


このライブラリを使用することによって画像やWebカメラから入力した人間の顔や目の位置などが、わずか20数行のコードで書くことができます。
今回はこれを使って背景画像を動的に更新し、そこに入ってきたオブジェクトを認識してみます。

1.処理の流れ

  • Webカメラデバイスハンドラを作成し画像をキャプチャ
  • 画像をopenCVで取り扱う画像形式からからRGBに変換し、色の極端に変化したところのしきい値により背景と前景を区別する
  • 前景と判定されたピクセルのみを、出力画像にコピー

背景画像の更新はキャプチャされた画像を重みつき差分で前回画像と合成し、それを比較することで背景とし、それとは異なる値を前景として出力します。 処理の流れはこのような感じですが、あまり注意すべきこともないですので、コーディングしてみます。

2.ソースコード

//main.cpp
#include <stdio.h>
#include <cv.h>
#include <highgui.h>
#include <tchar.h>
#pragma comment(lib, "cv.lib")
#pragma comment(lib, "cvaux.lib")
#pragma comment(lib, "cvcam.lib")
#pragma comment(lib, "cvhaartraining.lib")
#pragma comment(lib, "highgui.lib")

#define ALPHA    0.1 // 背景合成の重み
#define RGB_DIFF 30  // RGBの合計の差がこれ以下で前景判定

int main(int argc, char *argv[])
{
    IplImage *frame, *new_image, *bg_image, *fg_image, *mask_image;
    CvCapture *captureDev;

    // デバイスハンドラの作成
    if(!(captureDev = cvCaptureFromCAM(0))){
        MessageBox(NULL, _T("デバイスハンドラの作成に失敗しました。"), _T("エラー"), MB_ICONEXCLAMATION);
        return (-1);
    }

    // 初期化 
    // 画面サイズを得るために一度キャプチャします
    if(!(frame = cvQueryFrame(captureDev))){
        MessageBox(NULL, _T("画面サイズの取得に失敗しました。"), _T("エラー"), MB_ICONEXCLAMATION);
        return(-2);
    }

    cvNamedWindow("input", CV_WINDOW_AUTOSIZE);
    cvNamedWindow("output", CV_WINDOW_AUTOSIZE);

    const int width = frame->width;
    const int height = frame->height;
	
    // カメラからのキャプチャ画像をRGB変換した画像
    new_image = cvCreateImage( cvSize( width, height), IPL_DEPTH_8U, 3);
    // マスク画像
    mask_image = cvCreateImage( cvSize( width, height), IPL_DEPTH_8U, 1);
    // 前景画像
    fg_image = cvCreateImage( cvSize( width, height), IPL_DEPTH_8U, 3); 
    // 背景画像
    bg_image = cvCreateImage( cvSize( width, height), IPL_DEPTH_32F, 3);

    // メインループ
    while( cvWaitKey(1) != 'q' ){
        // キャプチャ
        if( !(frame = cvQueryFrame(captureDev)) )
        {
            MessageBox(NULL, _T("キャプチャに失敗しました。"), _T("エラー"), MB_ICONEXCLAMATION);
            goto FINALIZE;
        }

        // frameの内容をRGB変換
        cvCvtColor(frame, new_image, CV_HSV2RGB);
        // マスク画像作成
        for (int i = 0; i < width * height; i++){

            unsigned char *uc_new_ptr = (unsigned char*)&new_image->imageData[i * 3]; // カメラ画像のRGB画素ポインタ
            unsigned char new_b = uc_new_ptr[0];
            unsigned char new_g = uc_new_ptr[1];
            unsigned char new_r = uc_new_ptr[2];

            float *f_bg_ptr = (float*)&bg_image->imageData[(i * 3) * sizeof(float)]; // 背景画像のRGB画素ポインタ
            int bg_b = (int)f_bg_ptr[0];
            int bg_g = (int)f_bg_ptr[1];
            int bg_r = (int)f_bg_ptr[2];

            // 背景とカメラ画像の色の「距離」を計算
            int diff = abs((int)new_b - bg_b) 
                + abs( (int)new_g - bg_g)
                + abs( (int)new_r - bg_r);

            if(diff > RGB_DIFF)
                Mask_image->imageData[i] = 1; // 前景
            else
                mask_image->imageData[i] = 0; // 背景
        }

        // 背景差分で背景を更新する
        cvRunningAvg( new_image, bg_image, ALPHA);

        // 前景画像(のコピー先)を黒くする
        memset( fg_image->imageData, 0, width * height * 3);
        // 背景部分以外を前景画像にコピーする
        cvCopy( new_image, fg_image, mask_image); 

        // ウィンドウ表示
        // Webカメラからキャプチャした画像
        cvShowImage("input", frame);
        cvShowImage("output", fg_image);
    }

FINALIZE:
    // 終了処理
    cvDestroyWindow("input");
    cvDestroyWindow("output");
    cvReleaseImage( &new_image);
    cvReleaseImage( &fg_image);
    cvReleaseImage( &bg_image);
    cvReleaseImage( &mask_image);
    cvReleaseCapture(&captureDev);

    return(0);
}

3.実行結果

こちらが実行結果です。inputがキャプチャ画像、outputが背景と判断された部分を黒く表示しています。
image01.JPG

手をキャプチャしてみました。前景と判断された部分が判定されています。
image02.JPG

これらのように動的に背景画像を更新して、手のオブジェクトだけを抽出することができました。 機会がありましたら、次は手のモーションが何を行っているかなどの判定も行ってみようかと思います。

コメント (7)

> わずか20数行のコードで書くことができます
そんなことより、精度とパフォーマンスなどについての方が重要だと思うのですが。

> 背景画像を動的に更新し、そこに入ってきたオブジェクトを認識してみます。
> 画像をRGB変換し、しきい値により背景と前景を区別する
意味不明です。 その上、ほぼ全てのことにおいて説明不足です。 もう少しきちんとした説明を書いてくれませんか?

> デバイスを作成し
デバイスハンドラの間違いでは?

> unsigned char *new_ptr = (unsigned char*)&new_image->imageData[i * 3];
> float *bg_ptr = (float*)&bg_image-imageData[(i * 3) * sizeof(float)];
随分とひどいコードですね。 float か unsigned char かは、cvCreateImage の第 2 引数に対応しているんでしょうが、せめてマクロなどを使って型やデータ長に解りやすい名前を付けておいた方がいいと思います。

>> わずか20数行のコードで書くことができます
>そんなことより、精度とパフォーマンスなどについての方が重要だと思うのですが
今回は技術紹介なつもりでしたので、不要かな・・・と思いました。
精度としてはカメラのフレーム毎にxmlのデータ(SDK付属のhaarcascade_frontalface_default.xml)と比較を行うので、そちらの書き換えに応じてあげれるかと。

>> 背景画像を動的に更新し、そこに入ってきたオブジェクトを認識してみます。
>> 画像をRGB変換し、しきい値により背景と前景を区別する
>意味不明です。 その上、ほぼ全てのことにおいて説明不足です。 もう少しきちんとした説明を書いてくれませんか?
本文に追加しました。

>> unsigned char *new_ptr = (unsigned char*)&new_image->imageData[i * 3];
>> float *bg_ptr = (float*)&bg_image-imageData[(i * 3) * sizeof(float)];
>随分とひどいコードですね。 float か unsigned char かは、cvCreateImage の第 2 引数に対応しているんでしょうが、せめてマクロなどを使って型やデータ長に解りやすい名前を付けておいた方がいいと思います。
一部直しました。

>>そんなことより、精度とパフォーマンスなどについての方が重要だと思うのですが
>今回は技術紹介なつもりでしたので、不要かな・・・と思いました
技術紹介ならばそこが一番重要だと思うんですが?


>>意味不明です。 その上、ほぼ全てのことにおいて説明不足です。 もう少しきちんとした説明を書いてくれませんか?
> 本文に追加しました。
いや、相変わらず直ってませんね。
色の極端に変化したというのは、何と何を比べて言っているのかを書くべきです。

それと、背景画像だけ float 型を使っているのは、背景をアルファ値を使って更新する際に小数値が出て、それが切り捨てられると、うまく更新されないからなんですかね。

全体的に説明不足だと思います。


>> せめてマクロなどを使って型やデータ長に解りやすい名前を付けておいた方がいいと思います。
> 一部直しました
いや、直ってませんよね?
例えば私なら、
// 色要素を格納するデータ型
typedef float BG_COLOR;
// 色要素を格納するデータ型のサイズ (sizeof(BG_COLOR) と同じ)
#define BG_COLOR_SIZE IPL_DEPTH_32F
//色要素の個数 (R, G, B)
#define BG_COLOR_NUM 3

//初期化
bg_image = cvCreateImage(cvSize(width, height), BG_COLOR_SIZE, BG_COLOR_NUM);

//使用
BG_COLOR *bg_data = (BG_COLOR*)bg_image->imageData;
BG_COLOR *bg_ptr = bg_data + i * BG_COLOR_NUM;
int bg_b = (int)bg_ptr[0];

という風に書きますね。 さらに、BG_COLOR * から RGB のそれぞれの値を取り出す関数を定義すると思います。
こうすることで、float を double にしたい時などに変更が楽になりますし、可読性も増します。
このような記事では、コードの解り易さが大切だと思うので、何の説明もないマジックナンバーの使用や、解りづらいキャストや配列アクセスは控えた方がいいと思います。

斎藤さんの以下のご指摘

> //色要素の個数 (R, G, B)
> #define BG_COLOR_NUM 3

ですが、色深度のビット数 24 およびバイト数 3 はプログラム中に直接記述してしまってもそれほど問題ないと思います。
敢えてマクロで名前を付けるなら、

#define BPP 24

としておいて、バイト数が必要なところでは (BPP/8) などと書くとよいでしょう。
また、

> // 色要素を格納するデータ型
> typedef float BG_COLOR;

とのことですが、これも特に typedef するほどのものでもないと思います。
ただし、画像ごとの色深度の違いとその理由については本文中できちんと言及する必要があります。

個人的には、

typedef unsigned char BYTE;

などとしてもらうと読みやすいと思います。

「マスク画像作成」の for ループ

for(int i = 0; i < width * height; ++i)

ですが、これはマズいような気がします。
水平方向のピクセル値は連続領域に配置されるとしても、垂直方向に変化する際はパディングがある可能性が考えられます。
プログラムの始めの方で呼び出されている cvCreateImage の第一引数が、

width * height

ではなく

cvSize(width, height)

となっているのはおそらくそのためでしょう。
だとすると、このループも垂直方向と水平方向で分割して

for (int j=0; j<height; ++j){
  for (int i=0; i<width; ++i){
     unsigned char* uc_new_ptr =(unsigned char*)(ピクセル (0, j) のアドレスを取るなんらかの変数) + 3*i;

のようにする必要があるのではないでしょうか。

>>3-4
説明不足の点と可読性の点、理解しました。
きっちり判断できるように修正しておきます。

>>5
その可能性については特に考慮していませんでした。
垂直方向と水平方向で分離したほうがよさそうですので直しておきます。

「マスク画像作成」の forループ は、私ならばこう書きます。

途中で int diffBlue など、可読性のために必要の無い変数を定義したりしていますが、
それでもこの方が配列のインデックス計算の掛け算分でいくらか速いと思います。


// カメラ画像
unsigned char *camImg = (unsigned char*)(new_image->imageData);

// 背景画像
float *bgImg = (float*)(bg_image->imageData);

// マスク画像
unsigned char *dstImg = (unsigned char*)(mask_image->imageData);

for (i = 0; i < width * height; i++){

   // カメラ画像 i番目ピクセルのRGB画素
   int camPixBlue   = (int)camImg[0];
   int camPixGreen = (int)camImg[1];
   int camPixRed    = (int)camImg[2];

   // 背景画像 i番目ピクセルのRGB画素
   int bgPixBlue   = (int)bgImg[0];
   int bgPixGreen = (int)bgImg[1];
   int bgPixRed    = (int)bgImg[2];


   // i番目ピクセルでの カメラと背景のRGB色差
   int diffBlue   = abs(camPixBlue - bgPixBlue);
   int diffGreen = abs(camPixBlue - bgPixBlue);
   int diffRed    = abs(camPixBlue - bgPixBlue);

   // ピクセルのR,G,B要素 それぞれの差を足し合わせた物を 「色距離」とする
   int diffSum = diffBlue + diffGreen + diffRed;


   // 「色距離」と閾値を比較し 前景と背景を判断する
   if (diffSum > RGB_DIFF)
     *dstImg = 1; // 前景
   else
     *dstImg = 0; // 背景

   //ポインタは次の要素を指す
   ++camImg;
   ++bgImg;
   ++dstImg;

}

# よね?

コメントを投稿

コメントの公開は承認制のため、投稿から掲載までに時間がかかることがあります。


About

2008年01月17日 22:45 に投稿されたエントリです。

他にも多くのエントリがあります。
メインページアーカイブページもご覧ください。