2007.3.18.
石立 喬

Visual C++ 2005 Express Editionを用いた易しい画像処理(10)

―――― 二つのファイルを読み込み、ずれを補正して差分を表示する――――


 二つの画像ファイルを読み込み、その違いを求める方法を述べる。これは、不審者の発見などのセキュリティ、見本と比較して不良品を発見する製品検査、医療画像の比較による病巣部発見などに応用できる。

概要
 OpenFileDialogクラスを使用して、二つの画像ファイル(JPG、GIF、BMPなど)を個別に読み込み、Bitmapクラスとして保存し、差分を取る。二つの画像が完全に同じ位置から撮った同じ範囲のものであれば比較は簡単であるが、一般には、横(X方向)や縦(Y方向)にずれていることが多いので、あらかじめこれを補正する。斜めに傾いている場合は少し面倒であるので考慮しない。

画像比較の困難さ
 二つの画像のRGBデータを取得し、それの差分を取ることは簡単である。しかし、それをセキュリティなどの目的に実用化しようとすると、困難が多く発生する。
 監視されるべき画像は、屋外の場合は太陽の位置や天候、室内の場合は光源のオンオフなどによって光の当り具合が変わり、色が変わり影の位置も変化する。この問題を避けるには、常に一定の基準画像と比較するのではなく、比較的直近の画像と比較すれば良いが、この場合は動体検出となる。
 比較の基準となる画像と監視されるべき画像の撮像位置が正確に同一とはいえない。低解像度の画像システムを用いた場合、僅かな位置のずれが量子化されて大きな変化と読み取られる場合もある。これは、平滑化フィルタ(ぼかし)で除去できるが、一方で微細な変化を見逃す恐れがある。
 二つの画像のRGBデータの差分を取るとしても、その差分がいくら以上になれば変化があったと判定するかにも問題がある。R、G、Bの各変化分の平均で判断するのか、それらの最大値を用いるのかなどがあり、白っぽい背景に原色の物体が現れたような場合は前者が敏感で、黒っぽい背景に原色の物体が現れたような場合には後者が敏感である。ここでは、RGBの各変化分の最大値を用いた。

プログラムの概要
 プログラムは、次の部分から成っている。ただし、背景画像(A画像)をbmap_a、監視画像(B画像)をbmap_b、差分として認識した画像をbmap_cとする。
 なお、INT_MAX(=2147483647=231-1)を使用するため、<limits.h>をインクルードしておく。
◎Form1起動時に実行するもの
1) bmap_aとbmap_bをインスタンス化する。
2) bmap_aを読み込んだことを表すflag_aと、 bmap_bを読み込んだことを表すflag_bをそれぞれfalseに設定する。
◎Form1が画面に表示される度に実行するもの
1) ずれによるエラーを格納する配列error_amount[11,11]をクリヤーする。
2) bmap_aとbmap_bが共に読み込まれている時(flag_aとflag_bが共にtrue)は、以下を実行する。
  ・ x座標方向、y座標方向にp(範囲は-5〜5)とq(範囲は-5〜5)だけbmap_bをずらして、bmap_aと比較し、エラーを配列error_amount[p+5,q+5]に格納する。
  ・ 配列error_amount[p+5,q+5]が最小になるpとqを求め、それをerror_x、error_yに入れる。
  ・ bmap_bをerror_x、error_yだけずらして、二つの画像の差分を取り、差分の絶対値の最大を求め、それが試行錯誤的に決めた閾値76を超えた場合には、bmap_cに白を書き込み、以下の場合は黒を書き込む。
3) bmap_a、bmap_b、bmap_cと、ずれの量error_xおよびerror_yを画面に表示する。
◎メニューが選択された時に実行するもの
 「背景画像(A)」をクリックすると、menuARead_Click()により、指定されたファイルをbmap_aに読み込み、flag_aをtrueにし、画面描画を行なう。
「JPGファイル(*.jpg)|*.jpg|GIFファイル(*.gif)|*.gif|BMPファイル(*.bmp)|*.bmp」では、「|*.jpg|」などを記述する際に、「| *.jpg|」のようにスペースを前に入れるのは許されるが、「|*.jpg |」のように後ろに入れてはいけない。
 「背景画像(B)」では、ファイルをbmap_bに読み込み、flag_bをtrueにして画面描画をする。

プログラム

static int WIDTH=320,HEIGHT=240;
Bitmap ^bmap_a,^bmap_b;
bool flag_a,flag_b;

private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {

  bmap_a=gcnew Bitmap(WIDTH,HEIGHT);
  bmap_b=gcnew Bitmap(WIDTH,HEIGHT);
  flag_a=false;
  flag_b=false;

}

//背景画像の読込み
private: System::Void menuARead_Click(System::Object^ sender, System::EventArgs^ e) {

  OpenFileDialog^ ofdlg_a=gcnew OpenFileDialog();
  ofdlg_a->Filter="JPGファイル(*.jpg)|*.jpg|GIFファイル(*.gif)|*.gif|BMPファイル(*.bmp)|*.bmp";
  if(ofdlg_a->ShowDialog()==System::Windows::Forms::DialogResult::OK){
    bmap_a=gcnew Bitmap(ofdlg_a->FileName);
    flag_a=true;
    Invalidate();
  }
  else
    return;

}

//監視画像の読込み
private: System::Void menuBRead_Click(System::Object^ sender, System::EventArgs^ e) {

  OpenFileDialog^ ofdlg_b=gcnew OpenFileDialog();
  ofdlg_b->Filter="JPGファイル(*.jpg)| *.jpg|GIFファイル(*.gif)|*.gif|BMPファイル(*.bmp)|*.bmp";
  if(ofdlg_b->ShowDialog()==System::Windows::Forms::DialogResult::OK){
    bmap_b=gcnew Bitmap(ofdlg_b->FileName);
    flag_b=true;
    Invalidate();
  }
  else
    return;

}

private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) {

  Graphics^ gr=e->Graphics;

  int X0=10,Y0=38;
  int X1=340,Y1=38;
  int X2=10,Y2=288;
  int X3=10,Y3=538;

  int i,j;
  Color color_a,color_b,color_c;
  int ra,ga,ba,rb,gb,bb,rc,gc,bc;
  int d;
  int p,q;
  int error_min,error_x,error_y;
  array<int,2>^ error_amount=gcnew array<int,2>(11,11);
  Bitmap^ bmap_c=gcnew Bitmap(WIDTH,HEIGHT);

  if(flag_a && flag_b){     //二つの画像が共に読み込まれている場合のみ実行する

    //エラー配列error_amountのクリヤー
    for(q=-5;q<=5;q++)
      for(p=-5;p<=5;p++)
        error_amount[p+5,q+5]=0;

    //エラー量の累積
    for(q=-5;q<=5;q++)
      for(p=-5;p<=5;p++)
        for(j=5;j<HEIGHT-5;j+=5)
          for(i=5;i<WIDTH-5;i+=5){
            color_a=bmap_a->GetPixel(i,j);     //背景画像はそのまま
            ra=color_a.R;
            ga=color_a.G;
            ba=color_b.B;
            color_b=bmap_b->GetPixel(i+p,j+q);  //監視画像はずらす
            rb=color_b.R;
            gb=color_b.G;
            bb=color_b.B;
            //誤差の絶対値を累加算する
            error_amount[p+5,q+5]+=Math::Abs(rb-ra)+Math::Abs(gb-ga)+Math::Abs(bb-ba);
          }

    //エラーが最小となるずれを探す
    error_min=INT_MAX;
    for(q=-5;q<=5;q++)
      for(p=-5;p<=5;p++){
        if(error_amount[p+5,q+5]<error_min){
          error_min=error_amount[p+5,q+5];
          error_x=p;
          error_y=q;
        }
      }

    //ずれを考慮して二つの画像の差分を求める
    for(j=5;j<HEIGHT-5;j++)
      for(i=5;i<WIDTH-5;i++){
        //背景画像のRGBを取得する
        color_a=bmap_a->GetPixel(i,j);
        ra=color_a.R;
        ga=color_a.G;
        ba=color_a.B;
        //監視画像のRGBを取得する
        color_b=bmap_b->GetPixel(i+error_x,j+error_y);
        rb=color_b.R;
        gb=color_b.G;
        bb=color_b.B;
        //各RGBの差の絶対値をとる
        rc=Math::Abs(rb-ra);
        gc=Math::Abs(gb-ga);
        bc=Math::Abs(bb-ba);
        //RGBの差の絶対値の最大を求め、閾値と比較する
        d=Math::Max(Math::Max(rc,gc),bc);
        if(d>76) d=255;
        else d=0;
        color_c=Color::FromArgb(d,d,d);
        bmap_c->SetPixel(i,j,color_c);
      }

    gr->DrawImage(bmap_c,X2,Y2,WIDTH,HEIGHT);   //差分検出画像を表示する
    String^ string1=String::Format("X方向のシフト={0}ピクセル、Y方向のシフト={1}ピクセル",error_x,error_y);
    System::Drawing::Font^ font1=gcnew System::Drawing::Font("MSゴシック",10);
    gr->DrawString(string1,font1,Brushes::Black,X3,Y3);
  }

  gr->DrawImage(bmap_a,X0,Y0,WIDTH,HEIGHT);     //背景画像を表示する
  gr->DrawImage(bmap_b,X1,Y1,WIDTH,HEIGHT);     //監視画像を表示する

}

実行画面
 図1は、ビデオカメラで撮影した二つのコマを比較したもので、手ブレのせいか、少しずれがある。旗が風ではためき、影の形が変わっている。道路上の白い中心線は、ずれの補正のみでは十分除ききれなかった。ただし、これらは平滑化フィルタを用いれば除去できる。


図1 実行画面の例、すでに二つの画像を読み込んだ後であるが、参考のためにメニューを表示させた


 図2は、ずれの自動補正の効果を確認したもので、補正をしないと、より多くのノイズが発生する。


図2 ずれの自動補正をさせない場合


 図3は、差分の検出に当って、RGBの各差分量の最大値d=Math::Max(Math::Max(rc,gc),bc) の代わりに、RGBの各差分量の平均d=(rc+gc+bc)/3を使ったもので、大差が無いともいえるが、この画像の例では、やや劣るようである。


図3 RGBの各差分量の平均を用いた場合