2010.12.26.
石立 喬

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

――コンボリューションを用いた画像の平滑化、鮮鋭化、エッジ検出――

 「易しい画像処理(4)」までに紹介した、ピクセル毎の単独処理とは別に、近傍ピクセル値を含めて演算を行う画像処理がある。この代表的なものが、コンボリューションと呼ぶ処理で、原画像の平滑化(ノイズを除去したり、ボカしたりする)、鮮鋭化(ボケた画像をシャープにする)、エッジ検出(線画のように輪郭を抽出する)などに広く用いられる。ここでは、コンボリューションのメソッド実装に重点を置き、カーネルの値の選び方などは、簡単に紹介するにとどめる。

コンボリューション(Convolution)
 コンボリューションは、「畳み込み」と訳され、図1のように、原画像とカーネル(Kernel、核心部)の間で積和演算を行うことである。
 図の例では、カーネルのサイズを3x3で説明しているが、必要に応じて5x5、7x7などの大きなカーネルを使う場合もある。図1の左は原画像の一部で、処理対象であるe点を中心に合計9個のピクセルの値を使用する。それぞれの値に、図の中央に示したカーネルの対応する位置の値を乗じて合計し、図の右の、コンボリューション処理後の[e]点のピクセルの値とする。


図1 コンボリューションの方法

カーネルのいろいろ
 カーネルには、用途に応じて色々な値のマトリックスが用いられる。図2は、その一例で、他にも縦方向、横方向専用のエッジ検出などがあり、ガウスぼかしでσを小さくしたい場合には、さらに大きいマトリックスが必要になる。


図2 オペレータの一例


原画像周辺拡張の必要性
 原画像をコンボリューションするに当たっては、対象ピクセルの周辺のピクセル値が必要になるが、画像の周辺部においては、周辺ピクセルの無い状態が生じる。これを防止するために、原画像よりも一回り大きい拡張画像をあらかじめ用意し、それに対してコンボリューションを行う。
 図3を用いて説明すると、図3で、上の図の白いマスで囲まれた範囲は拡張画像を示し、グレイで示した中央の原画像より一マスだけ外側に設定されている(3 x 3カーネルの場合)。やや濃いグレイで示したのは原画像の周辺部で、この部分が拡張画像にコピーされることを示す。

図3 原画像から拡張画像を作る方法


Java SE 6 によるコンボリューション

 Java Standard Edition 6 には、java.awt.imageパッケージにConvolveOpクラスと、コンボリューション用マトリックスのためのKernelクラスがある。原画像の周辺に対する処理は、一旦拡張してからコンボリューションを行うのではなく、周辺は無視し、コンボリューション後の画像に原画像の周辺をそのままコピーするEDGE_NO_OPと、コンボリューション後の画像の周辺を、無条件にゼロにするEDGE_ZERO_FILLとがある。
 具体的には、次のように実行する。
 あらかじめ、matrix(一例)と言う名称をつけて3 x 3の配列を用意しておく。これを用いて、kernel1と言うKernelクラスを生成し、さらに、コンボリューションの条件を付加してconvop1と言うConvolveOpクラスを作る。コンボリューション実行には、convop1クラスのメソッドfilterを使う。
   Kernel kernel1=new Kernel(3,3,matrix);   //3,3はマトリックスのサイズ
   ConvolveOp convop1=new ConvolveOp(kernel1,ConvolveOp.EDGE_NO_OP,null); //周辺は原画像のままとする
   BufferedImage bimg_dst=convop1.filter(bimg_src,null);
 便利なクラスやメソッドではあるが、周辺の処理に難点がある。


プログラムの概要
◎メインプログラムは、次の部分からなっている。
1)平滑化(ぼかし)、鮮鋭化、エッジ検出(線画化)の3種類のカーネルを定義する。
2)原画像をファイルから読み込みbmap_srcとする。
3)原画像を左上に表示する。
4)平滑化と鮮鋭化のコンボリューションを行い、bmap_blur、bmap_sharpを得て、それぞれを画面上の右上、左下に表示する。
5)エッジ検出のコンボリューションを行い、bmap_edgeを得る。
6)bmap_edgeの各ピクセルを調べ、R、G、B各成分のいずれかがしきい値(ここでは20を使用)を超える場合は濃度dを0(黒)に、それ以外は255(白)に設定して、新しいbmap_edgeを得る。
7)bmap_edgeを右下に表示する。
◎コンボリューション用のメソッドは、次の部分からなっている。ただし、簡単のために、カーネルのサイズは3x3に固定してある。
1)引数として与えられたbmapから、widthとheightを取得する。
2)原画像と拡張画像について、全ピクセルに対するColor構造体の配列color_srcとcolor_expを用意する。
3)color_srcからcolor_expを作成する。
4)color_expと、引数として与えられたkernelとの間でコンボリューションを実行する。
5)得られたピクセル値は、0〜255の範囲を逸脱する恐れがあるので、補正する。
6)最終的に得られたピクセル値によってbmap_dstを作成する。


プログラム

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;
   int i,j;
   Color color1;
   Byte r,g,b,d;

   array<float,2>^ kernel_blur={{0.11f,0.11f,0.11f},  //平滑化カーネル
                  {0.11f,0.12f,0.11f},
                  {0.11f,0.11f,0.11f}};

   array<float,2>^ kernel_sharp={{ 0.0f,-0.3f, 0.0f},  //鮮鋭化カーネル
                  {-0.3f, 2.2f,-0.3f},
                  { 0.0f,-0.3f, 0.0f}};

   array<float,2>^ kernel_edge={{ 0.0f,-1.0f, 0.0f},  //エッジ検出カーネル
                  {-1.0f, 4.0f,-1.0f},
                  { 0.0f,-1.0f, 0.0f}};

   Bitmap^ bmap_src=gcnew Bitmap(WIDTH,HEIGHT);
   Bitmap^ bmap_blur=gcnew Bitmap(WIDTH,HEIGHT);
   Bitmap^ bmap_sharp=gcnew Bitmap(WIDTH,HEIGHT);
   Bitmap^ bmap_edge=gcnew Bitmap(WIDTH,HEIGHT);

   //画像ファイルを読み込んで原画像にする
   bmap_src=gcnew Bitmap("sample.JPG");
   //原画像を左上に表示する
   gr->DrawImage(bmap_src,X0,Y0);
   //カーネルkernel_blurでコンボリューションする
   bmap_blur=makeConvolution(bmap_src,kernel_blur);
   //平滑化画像を右上に表示する
   gr->DrawImage(bmap_blur,X1,Y0);
   //カーネルkernel_sharpでコンボリューションする
   bmap_sharp=makeConvolution(bmap_src,kernel_sharp);
   //鮮鋭化画像を左下に表示する
   gr->DrawImage(bmap_sharp,X0,Y1);
   //カーネルkernel_edgeでコンボリューションする
   bmap_edge=makeConvolution(bmap_src,kernel_edge);

   //bmap_edge画像をしきい値を用いて白黒画像にする
   for(j=0;j<HEIGHT;j++)
      for(i=0;i<WIDTH;i++){
         color1=bmap_edge->GetPixel(i,j);
         r=color1.R;
         g=color1.G;
         b=color1.B;
         if(r>20 || g>20 || b>20) d=0;
         else           d=255;
         bmap_edge->SetPixel(i,j,Color::FromArgb(d,d,d));
      }

   //エッジ検出画像を右下に表示する
   gr->DrawImage(bmap_edge,X1,Y1);

}

private:Bitmap^ makeConvolution(Bitmap^ bmap,array<float,2>^ kernel){

   int width=bmap->Width;
   int height=bmap->Height;
   int i,j,m,n;
   Color color1;
   int r,g,b;   //Byteでは、オーバーフローすることがあるのでいけない
   float r1,g1,b1;
   Bitmap^ bmap_exp=gcnew Bitmap(width+2,height+2);
   Bitmap^ bmap_dst=gcnew Bitmap(width,height);
   array<Color,2>^ color_src=gcnew array<Color,2>(width,height);    //原画像のデータ
   array<Color,2>^ color_exp=gcnew array<Color,2>(width+2,height+2);  //拡張画像のデータ

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

   //中心部は原画像を拡張画像にコピーする
   for(j=0;j<height;j++)
      for(i=0;i<width;i++)
         color_exp[i+1,j+1]=color_src[i,j];
   //周辺部は拡張画像内でコピーする
   //上辺と下辺
   for(i=0;i<width+2;i++){
      color_exp[i,0]=color_exp[i,1];
      color_exp[i,height+1]=color_exp[i,height];
   }
   //左辺と右辺
   for(j=0;j<height+2;j++){
      color_exp[0,j]=color_exp[1,j];
      color_exp[width+1,j]=color_exp[width,j];
   }

   //拡張画像color_expとカーネルkernelでコンボリューションする
   for(j=0;j<height;j++)
      for(i=0;i<width;i++){
         r1=0.0f;g1=0.0f;b1=0.0f;
         for(n=-1;n<=1;n++)
            for(m=-1;m<=1;m++){
               color1=color_exp[i+m+1,j+n+1];
               r1+=color1.R*kernel[m+1,n+1];
               g1+=color1.G*kernel[m+1,n+1];
               b1+=color1.B*kernel[m+1,n+1];
            }

         r=(int)r1;
         if(r<0)     r=0;
         else if(r>255) r=255;
         g=(int)g1;
         if(g<0)     g=0;
         else if(g>255) g=255;
         b=(int)b1;
         if(b<0)     b=0;
         else if(b>255) b=255;

         bmap_dst->SetPixel(i,j,Color::FromArgb(r,g,b));
   }

   return bmap_dst;

}


得られた結果
 図4の左上は原画像で、意図的にソフトな画像を使用している。右上は原画像を平滑化した画像で、さらにソフトになっている。左下は、鮮鋭化した画像で、原画像がソフトにもかかわらず、シャープに見える。右下は、原画像のエッジを検出し、一定量以上の大きさの場所を黒く表示している。簡単なエッジ検出カーネルを使用したので、十分満足な画像が得られたとは言えない。もっと優れたエッジ検出については別途紹介する。


図4 得られた結果


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