2007.4.2.
石立 喬

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

――― 分かり易いアルゴリズムで二値画像を細線化する―――

 画像処理では、平滑化、鮮鋭化、エッジ検出などに次いで、細線化も良く用いられる。文字や物体を認識するには、それらの特徴を抽出する必要があり、太い線や塊では判別し難いからである。

概要
 二値化された原画像を4方向から順次スキャンし、黒のピクセルを囲む周辺の8個のピクセルを調べて、3種類のパターン条件のうち、少なくとも一つに合致すれば、その中心の黒のピクセルを白に変して消去する。これを、消去が生じなくなるまで繰り返す。

細線化の必要性
 細線化は、ThinningやSkeletonizationと呼ばれ、二値画像を幅1ピクセルの線画像に変換する処理である。短くなったり、線が途中で切れたり、孔が開いたりしてはいけない。得られた線は、元の図形の幅の中心に来ることが望ましい。
 エッジ検出によって得られたエッジ画像は、元の画像の変化が比較的緩やかな所では、幅が広くなる。エッジ検出の閾値を高くすると幅は狭くなるが、より緩やかな変化を検出できない。したがって、エッジ検出処理後に細線化を行って、文字認識やパターン認識に使うことが多い。

細線化のアルゴリズム
 細線化の手法は古くから研究され、種々の方法が知られている。ここでは、筆者による比較的分かりやすい方法を使用した。細線化の結果として得られた線が元の図形の中心部に来るように、左上→右下、右下→左上、右上→左下、左下→右上の順に周囲から狭めてゆく。消去できるかどうかを判断すべき1(黒)の点を中心に、左右または斜め方向の片方の半分が1(黒)で他方が0(白)の場合は、判断すべき点を0(白)にしても良いと考える。Hilditchのアルゴリズムに似ている点もあるが、線が途切れる欠点が無く、プログラムが単純化できる。

◎注目するピクセルとその周辺の呼び方
 図1に示すように、消去できるかどうかを調べるピクセルをcenterとし、周囲のピクセルは、反時計回りに、p[0]からp[7]と呼ぶ。center、p[0]〜p[7]は、このプログラムでは、true(黒)またはfalse(白)の値を持つbool型にしてある。ただし、特にcenterと言う変数(オブジェクト)名は用いない。


図1 消去できるかどうかを判定する周辺ピクセルの名称


◎画面左上から右下に向かって探索する場合に使用するパターン
 画面左上から右下に向かって探索する場合には、左上が白(0)に、右下が黒(1)になっている場合を探し、下記のような状態の場合は中央のピクセル(丸で囲まれている)を1から0に変更する。XはDon't care(どちらでも良い)を示す。


図2 左上から右下に向かって探索する場合に消去できる条件


 この条件を表す式は、左から
    p[2]*p[3]*p[4]=1 で、かつ p[6]+p[7]+p[0]=0
    p[3]*p[4]*p[5]=1 で、かつ p[7]+p[0]+p[1]=0
    p[4]*p[5]*p[6]=1 で、かつ p[0]+p[1]+p[2]=0
である。
 実際にプログラム上で記述する場合には、p[0] 〜 p[8]はbool型であるので、たとえば、「p[2]*p[3]*p[4]=1 で、かつ p[6]+p[7]+p[0]=0」の代わりに、「(p[2] && p[3] && p[4]) && !(p[6] || p[7] || p[0])」のようにする。「!」はbool型の変数に対して否定の演算を行う。
 このような条件式は、添え字を1ずつ増やし(7の次は0)ながらforループで実行できる。上の例の場合には、添え字が2から始まるので、この値をstartと呼んでいる。1が3個並んでいる、反時計回りで最初のピクセルの添え字の値がstartであると考えると良い。
 上記の場合は、start=2 であり、プログラムでは、start=UPPRER_LEFT として指定する(UPPER_LEFTは定数2として、別途定義しておく)。

◎画面右下から左上に向かって探索する場合に使用するパターン


図3 右下から左上に向かって探索する場合に消去できる条件


 この条件を表す式は、左から
    p[6]*p[7]*p[0]=1 で、かつ p[2]+p[3]+p[4]=0
    p[7]*p[0]*p[1]=1 で、かつ p[3]+p[4]+p[5]=0
    p[0]*p[1]*p[2]=1 で、かつ p[4]+p[5]+p[6]=0
である。start=LOWER_RIGHTによって指定する(LOWER_RIGHT=2)。

◎画面右上から左下に向かって探索する場合に使用するパターン


図4 右上から左下に向かって探索する場合に消去できる条件


 この条件を表す式は、左から
    p[0]*p[1]*p[2]=1 で、かつ p[4]+p[5]+p[6]=0
    p[1]*p[2]*p[3]=1 で、かつ p[5]+p[6]+p[7]=0
    p[2]*p[3]*p[4]=1 で、かつ p[6]+p[7]+p[0]=0
である。start=UPPER_RIGHTによって指定する(UPPER_RIGHT=0)。

◎画面左下から右上に向かって探索する場合に使用するパターン


図5 左下から右上に向かって探索する場合に消去できる条件


 この条件を表す式は、左から
    p[4]*p[5]*p[6]=1 で、かつ p[0]+p[1]+p[2]=0
    p[5]*p[6]*p[7]=1 で、かつ p[1]+p[2]+p[3]=0
    p[6]*p[7]*p[0]=1 で、かつ p[2]+p[3]+p[4]=0
である。start=LOWER_LEFTによって指定する(LOWER_LEFT=4)。

プログラムの概要
◎ Form1_Paint()の内容
1)細線化したい二値原画像(幅WIDTH=320, 高さHEIGHT=240)をファイルから読み込み、Bitmapクラスのbmap_srcとし、パソコン画面左に表示する。
2)細線化を繰り返し実行する対象として、原画像の四辺をそれぞれ1ピクセルずつ拡張したBitmapクラスのbmap_thin(幅WIDTH+2,高さHEIGHT+2)を用意し、一旦全体を白で描画した後に、中央部にbmap_srcをコピーする。
3)bmap_thinから、細線化の対象とするbool型の配列target_pixels[i,j]を求める。
4)細線化処理として、change_flagがfalseのまま(変化がなかった)になるまで、以下を繰り返す。
  ・change_flagをfalseに設定する。
 以下は、左上→右下、右下→左上、右上→左下、左下→右上の順で4回実行する。
  ・メソッドcopyTargetToCopied()により、target_pixels[i,j]をcopied_pixels[i,j]にコピーする。これは、消去したピクセルを直ちに次の判断に使用しないためである。
  ・copied_pixels[i,j]をスキャンして、もしそれが黒(true)であれば、そのピクセルを消去できるかを判断するメソッドthinImage(i,j,UPPER_LEFT)などを呼び出す。最初はUPPER_LEFTを使用するが、順次、LOWER_RIGHT、UPPER_RIGHT、LOWER_LEFTと指定を変えてゆく。
  ・四方からの細線化が一回終わると、得られたtarget_pixels[i,j]からbmap_thinを作り、画面の右側に描画する。
4)thinImage(i,j,UPPER_LEFT)など、四方向の細線化をすべて実施してもchange_flagがtrueで返されない場合は、それ以上消去するピクセルが無いことを表しているので、細線化の繰り返しを終了する。

◎メソッドthinImage(i,j,start)の内容
 このメソッドは、指定された場所の黒ピクセルを、条件により白に変更する。引数として、場所と、探索の方向(左上→右下をあらわすUPPER_LEFTなど)が与えられる。
1)指定された場所(i,j)の周辺の8個のピクセルの値を、配列copied_pixelsから配列pにコピーする。
2)startによって指定された連続した3個のピクセルが黒(true)で、かつcenterに対して点対象の反対側の連続した3個のピクセルが白(false)の場合には、centerすなわち指定された場所(i,j)に対応するtarget_pixels[i,j]をfalseに設定し(消去する)、消去が行われたことを示すchange_flagをtrueに設定する。それ以上調べる必要は無いので、直ちにreturnで呼び出し元に戻る。

プログラム

array<bool,2>^ target_pixels;
array<bool,2>^ copied_pixels;
bool change_flag;
static int WIDTH=320,HEIGHT=240;

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

  copied_pixels=gcnew array<bool,2>(WIDTH+2,HEIGHT+2);
  target_pixels=gcnew array<bool,2>(WIDTH+2,HEIGHT+2);

}

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

  Graphics^ gr=e->Graphics;

  int X0=10,Y0=10;    //原画像の描画位置
  int X1=339,Y1=10;   //細線化画像の描画位置

  int UPPER_LEFT=2,LOWER_RIGHT=6,UPPER_RIGHT=0,LOWER_LEFT=4;

  Color color1;
  int i,j;
  Bitmap^ bmap_src=gcnew Bitmap("A:/sample.gif");      //原画像
  Bitmap^ bmap_thin=gcnew Bitmap(WIDTH+2,HEIGHT+2);  //周辺拡張画像で細線化の対象

  gr->DrawImage(bmap_src,X0,Y0,WIDTH,HEIGHT);       //原画像を左に表示する

  //bmap_thinを全部白に設定する
  for(j=0;j<HEIGHT+2;j++)
    for(i=0;i<WIDTH+2;i++)
      bmap_thin->SetPixel(i,j,Color::White);

  //原画像をbmap_thinの中心にコピーする
  for(j=0;j<HEIGHT;j++)
    for(i=0;i<WIDTH;i++){
      color1=bmap_src->GetPixel(i,j);
      bmap_thin->SetPixel(i+1,j+1,color1);
    }

  //bmap_thinをtarget_pixels配列に変換する
  for(j=0;j<HEIGHT+2;j++)
    for(i=0;i<WIDTH+2;i++){
      color1=bmap_thin->GetPixel(i,j);
      if(color1.R<128) target_pixels[i,j]=true;
      else target_pixels[i,j]=false;
    }

  //細線化を実行する
  do{
    change_flag=false;

    //左上から
    copyTargetToCopied();
    for(j=1;j<=HEIGHT;j++)
      for(i=1;i<=WIDTH;i++)
        if(copied_pixels[i,j]) thinImage(i,j,UPPER_LEFT);

    //右下から
    copyTargetToCopied();
    for(j=HEIGHT;j>=1;j--)
      for(i=WIDTH;i>=1;i--)
        if(copied_pixels[i,j]) thinImage(i,j,LOWER_RIGHT);

    //右上から
    copyTargetToCopied();
    for(j=1;j<=HEIGHT;j++)
      for(i=WIDTH;i>=1;i--)
        if(copied_pixels[i,j]) thinImage(i,j,UPPER_RIGHT);

    //左下から
    copyTargetToCopied();
    for(j=HEIGHT;j>=1;j--)
      for(i=1;i<=WIDTH;i++)
        if(copied_pixels[i,j]) thinImage(i,j,LOWER_LEFT);

    //target_pixels[i,j]をbmap_thinに変換する
    for(int j=0;j<HEIGHT+2;j++)
      for(int i=0;i<WIDTH+2;i++){
        if(target_pixels[i,j]) bmap_thin->SetPixel(i,j,Color::Black);
        else    bmap_thin->SetPixel(i,j,Color::White);
      }

    //bmap_thnを表示する
    gr->DrawImage(bmap_thin,X1,Y1,WIDTH+2,HEIGHT+2);

  }while(change_flag);

}

private: void thinImage(int i,int j,int start){

  array<bool>^ p=gcnew array<bool>(8);
  bool product,sum;

  //周辺のデータをコピーする
  p[0]=copied_pixels[i-1,j-1];
  p[1]=copied_pixels[i-1,j];
  p[2]=copied_pixels[i-1,j+1];
  p[3]=copied_pixels[i,j+1];
  p[4]=copied_pixels[i+1,j+1];
  p[5]=copied_pixels[i+1,j];
  p[6]=copied_pixels[i+1,j-1];
  p[7]=copied_pixels[i,j-1];

  //周辺の黒(true)の三個連続と白(false)の三個連続があれば、そのピクセルを白(false)にする
  for(int k=start;k<start+3;k++){
    product=p[k % 8] && p[(k+1) % 8] && p[(k+2) % 8];   //黒(true)の三個連続でtrueになる
    sum=p[(k+4) % 8] || p[(k+5) % 8] || p[(k+6) % 8];     //白(false)の三個連続でfalseになる
    if(product && !sum){
      target_pixels[i,j]=false;         //ピクセルを黒(true)から白(false)に変更
      change_flag=true;            //変更があった(消去した)ことを知らせる
      return;
    }
  }

}

private: void copyTargetToCopied(void){

  for(int j=0;j<HEIGHT+2;j++)
    for(int i=0;i<WIDTH+2;i++)
      copied_pixels[i,j]=target_pixels[i,j];

}


得られた結果
 図6は英文字の場合の例で、左は細線化の前の二値原画像、右は細線化後の画像である。


図6 英文字の細線化例


 図7は図形の場合を示したもので、四方向から追って行くため、円形の図形に対してXの形が現れる。


図7 色々な図形の細線化例


 細線化を四方から行うのは必須条件であるが、参考までに、左上→右下のみの例を図7の左に、左上→右下と右下→左上のみの例を図7の右に示す。


図7 細線化を四方から行わなかった場合の例