2011. 1. 15.
石立 喬

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

――――エッジ検出と線画化 ――――


 画像処理の中では、画像理解やパターン認識の前処理として、エッジ検出が良く用いられる。文字や物体を認識するには、形状や特徴を抽出認知する必要があり、エッジは物体の形状を良く表すからである。

概要
 エッジ検出の手法は、画像処理の基本であり、多くの資料に詳述されている。すでに述べたコンボリューションカーネルとして、PrewittやSobelを使用する。出力結果に対するしきい値を小さくすればエッジが太くなり、しきい値を大きくすればエッジが検出されない。ここではPrewitt、Sobelの他、すでに「易しい画像処理(5)」で紹介したLaplacianを用いるプログラムを紹介する。 コンボリューション出力の処理方法を工夫すると、原画像を線画風に加工することもできる。

エッジ検出カーネル

 エッジ検出にはコンボリューションを用いる。エッジ検出は鮮鋭化の極端な例とも考えられ、鮮鋭化では、原画像において濃度の変化が大きい部分がその強調で出力されるのに対し、エッジ検出では、背景を白にして、変化部を黒で二値化して表示するのが一般的である。
 カーネルには、空間的な一次微分を用いるものと二次微分を用いるものがある。一次微分は方向性があり、垂直方向、水平方向などに分けて検出する必要がある。一次微分で有名なものにPrewittとSobelがあるが、一般的な画像では、差はほとんど感じられない。二次微分はLaplacianを用い、方向性がない便利さがあるが、ノイズに弱い。
 微分操作は、微細なノイズを強調する欠点があるので、あらかじめ原画像を平滑化しておいたり、カーネルに平滑化の機能を併せ持たせたりすることがある。
◎一次微分方式(PrewittとSobel)
 一次微分によると、画像のピクセル濃度が変化する部分では、増減の方向により正または負の出力が得られる。最も変化の大きい部分(エッジ)は、その絶対値が最大になる場所である。垂直、水平方向のエッジを求めるには、90°角度の異なる二つカーネルを必要とする。
◎二次微分方式(Laplacian)
 二次微分方式では、一次微分で生じた正または負の出力を更に微分するので、正出力に対しては、正−負のペア出力が、負出力に対しては、負−正のペア出力が得られる。単に絶対値をとるとエッジが二本になる場合がある。エッジのある場所は、上記正負ペアの中間点、すなわちゼロクロス点である。ゼロクロス点をを求めると、真のエッジが得られるが、処理が面倒な割には効果が少ない。代わりに、正負ペアの一方だけを使用することがあるが、エッジの場所が正確には表せない。
 図1のPrewittとSobelは一次微分方式のカーネルで、横方向のエッジ(水平方向の急激な変化)に反応する。Prewittは原理的に最も分かりやすく、適度な平滑化機能も含まれている。Sobelは中心部の重みを少し大きくして、対象とするピクセル付近に重点をおき、平滑化の効果を抑制している。実際には、これらを90°回転させたカーネルも使用して縦方向のエッジ(垂直方向の急激な変化)も検出し、縦横両方のエッジ出力を加算したり、大きい方を選んだりする。右端は二次微分方式のLaplacianの一例で、一つのカーネルで縦横両方向に同時に作用する。


図1 使用したカーネル


エッジの判定としきい値の決め方
 カーネルを用いたコンボリューションは、RGBの各成分についてそれぞれ実行する。縦方向と横方向の双方でコンボリューションを行うと、合計で6個の結果が得られる。これらは負を含む値になるので、絶対値をとる。
 各成分をコンボリューションした6個の結果に対して、
 1)すべての平均を採る。
 2)すべての最大を採る。
 3)縦方向と横方向に分け、それぞれの二乗和の平方根を採る。
などの方法があるが、比較の結果、ほとんど差がないことが分かり、結局、ここでは2)を採用した。
 この値を一定のしきい値と比較して、それを超えた部分をエッジと判定し、黒で表示し、しきい値以下の場合には白で表示して二値化する。ここでは、試行錯誤の結果、しきい値を160に固定した。

プログラムの構成
 主要部分はメソッドdetectEdgesに記述してある。このメソッドは、引数として、原画像bmap_srcと、X方向およびY方向の二種類のカーネルkernel_x、kernel_yを受け取り、
 1)原画像から、コンボリューション用のカラーデータcolor_srcを得る。
 2)周辺部(上辺、下辺、左辺、右辺)を除き、カラーデータとカーネルでコンボリューションを行う。
 3)コンボリューション結果のRGB各成分r、g、bについてX方向、Y方向のそれぞれ最大値を求め、さらに、それらの最大値を求める。
 4)濃淡を反転して検出結果の濃度をdとする。
 5)濃度dとしきい値を比較して、白または黒をエッジ検出結果画像bmap_dstに書き込む。
 6)bmap_dstの周辺をコピーして拡張する。
を実行する。
 5)を書き換えることによって、しきい値を変更し、結果の画像の濃度も変えることができる(白と黒のみでなく、グレイスケールにも)。

プログラム

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

  Graphics^ gr=e->Graphics;

  int WIDTH=320,HEIGHT=240;
  int X0=10,Y0=10;
  int X1=340,Y1=260;

  //Prewittカーネル
  array<int,2>^ prewitt_x={{ 1, 1, 1},
                { 0, 0, 0},
               {-1,-1,-1}};
  array<int,2>^ prewitt_y={{ 1, 0,-1},
               { 1, 0,-1},
               { 1, 0,-1}};
  //Sobelカーネル
  array<int,2>^ sobel_x={{ 1, 2, 1},
               { 0, 0, 0},
              {-1,-2,-1}};
  array<int,2>^ sobel_y={{ 1, 0,-1},
               { 2, 0,-2},
               { 1, 0,-1}};
  //Laplacianカーネル
  array<int,2>^ laplacian={{ 0,-2, 0},    //出力を上げるために係数を2倍してある
               {-2, 8,-2},
               { 0,-2, 0}};

  Bitmap^ bmap_prewitt=gcnew Bitmap(WIDTH,HEIGHT);
  Bitmap^ bmap_sobel=gcnew Bitmap(WIDTH,HEIGHT);
  Bitmap^ bmap_laplacian=gcnew Bitmap(WIDTH,HEIGHT);

  //原画像を読み込む
  Bitmap^ bmap_src=gcnew Bitmap("sample.PNG");
  //原画像を左上に表示する
  gr->DrawImage(bmap_src,X0,Y0);
  //Prewittでエッジを検出して画像bmap_prewittとする
  bmap_prewitt=detectEdges(bmap_src,prewitt_x,prewitt_y);
  //画像bmap_prewittを右上に表示する
  gr->DrawImage(bmap_prewitt,X1,Y0);
  //Sobelでエッジを検出して画像bmap_sobelとする
  bmap_sobel=detectEdges(bmap_src,sobel_x,sobel_y);
  //画像bmap_sobelを左下に表示する
  gr->DrawImage(bmap_sobel,X0,Y1);
  //Laplacianでエッジを検出して画像bmap_laplacianとする(引数の数を揃えるために2回記述)
  bmap_laplacian=detectEdges(bmap_src,laplacian,laplacian);
  //画像bmap_laplacianを右下に表示する
  gr->DrawImage(bmap_laplacian,X1,Y1);

}

private:Bitmap^ detectEdges(Bitmap^ bmap,array<int,2>^ kernel_x,array<int,2>^ kernel_y){   //sizeは3に固定

  int width=bmap->Width;
  int height=bmap->Height;
  int i,j,m,n;
  Color color1;
  int rx,gx,bx,ry,gy,by,d;   //Byteではいけない
  int x_max,y_max;
  Bitmap^ bmap_dst=gcnew Bitmap(width,height);
  array<Color,2>^ color_src= gcnew array<Color,2>(width,height);   //原画像のデータ

  //原画像bmapから原画像カラーデータcolor_srcを作る
  for(j=0;j<height;j++)
    for(i=0;i<width;i++)
      color_src[i,j]=bmap->GetPixel(i,j);

  //周辺を除いてコンボリューションし、エッジを判定して、bmap_dstに書き込む
  for(j=1;j<height-1;j++)
    for(i=1;i<width-1;i++){
      rx=0;gx=0;bx=0;
      ry=0;gy=0;by=0;
      for(n=-1;n<=1;n++)
        for(m=-1;m<=1;m++){
          color1=color_src[i+m,j+n];
          //X方向のカーネルでコンボリューション
          rx+=color1.R*kernel_x[m+1,n+1];
          gx+=color1.G*kernel_x[m+1,n+1];
          bx+=color1.B*kernel_x[m+1,n+1];
          //Y方向のカーネルでコンボリューション
          ry+=color1.R*kernel_y[m+1,n+1];
          gy+=color1.G*kernel_y[m+1,n+1];
          by+=color1.B*kernel_y[m+1,n+1];
        }
      x_max=Math::Max(Math::Max(Math::Abs(rx),Math::Abs(gx)),Math::Abs(bx));
      y_max=Math::Max(Math::Max(Math::Abs(ry),Math::Abs(gy)),Math::Abs(by));
      d=255-Math::Max(x_max,y_max);

   //----------- しきい値によるエッジ判定と二値化 ----------
      if(d>160)     d=255;
      else        d=0;
   //-------------------------------------------------------

      bmap_dst->SetPixel(i,j,Color::FromArgb(d,d,d));
    }

  //エッジ画像bmap_dstの周辺に内部をコピーする
  for(i=0;i<width;i++){
    //上辺
    color1=bmap_dst->GetPixel(i,1);
    bmap_dst->SetPixel(i,0,color1);
    //下辺
    color1=bmap_dst->GetPixel(i,height-2);
    bmap_dst->SetPixel(i,height-1,color1);
  }
  for(j=0;j<height;j++){
    //左辺
    color1=bmap_dst->GetPixel(1,j);
    bmap_dst->SetPixel(0,j,color1);
    //右辺
    color1=bmap_dst->GetPixel(width-2,j);
    bmap_dst->SetPixel(width-1,j,color1);
  }

  return bmap_dst;

}

得られた結果
 図2で、左上は、原画像、右上はPrewitt、左下はSobel、右下はLaplacianによるエッジ検出後の画像である。PrewittとSobelの差はほとんど認められないが、Sobelの方が、感度が高めに感じる(Sobelは中央部の係数が大きいので当然か)。 Laplacianはノイズが多く、やや不鮮明である。
 ただし、これらの結果は、しきい値を個別に最適化してなく、Laplacianの場合は、事前のガウスぼかしやゼロクロス検出をしていないので、あくまでも、このプログラムの結果であることに注意されたい。
 図3は、自然画像ではない文字や図形のエッジ検出の例で、PrewittとSobelに差異はないが、エッジ検出結果が、すべて2ピクセル幅になっている。これは、これらのカーネルの中心部分にゼロの領域があり、緩やかな検出をすることが原因である。Laplacianの場合は、斜めのエッジに対しては、1ピクセル幅になっている。
 検出結果の線幅を細くするには、結果に対して、さらに細線化処理を行えば良い。


図2 自然画像を用いて得られた画面



図3 二値化図形の場合


単純な二値化を行わないエッジ検出
 プログラムで、下記の部分を書き換えることにより、単なるエッジ検出とは異なる結果を得ることができる。

   //----------- しきい値によるエッジ判定と二値化 ----------
      if(d>160)     d=255;
      else        d=0;
   //-------------------------------------------------------
を、
      if(d>255)     d=255;
      else if(d<0)   d=0;
とすると、コンボリューション結果を反転しただけの中間調が、そのまま出力される。これを、図4の左に示す。
また、
      if(d>255)     d=255;
      else if(d<128)  d=128;
とすると、強い濃いエッジが薄めに表示され、図4の右に示すように、鉛筆によるスケッチのような線画化画像が得られる。

エッジ検出結果を反転してそのまま出力 濃い黒を抑えて出力

図4 完全な二値化を行わないと線画のようになる



「Visual C++ の勉強部屋」(目次)へ