ノイズの除去方法
赤の抽出結果は、二値画像として得られます。赤と判断された交通標識以外の物体がノイズとして含まれていますので、これを除去する必要があります。大きいノイズは、形状による認識でしか除外できませんが、小さいノイズは比較的簡単に除去できます。
中間調のある画像のノイズ除去には、一般に線形の平滑化(平均化)フィルタが用いられます(筆者によるCodeZineの『コンボリューションを用いた画像の平滑化、鮮鋭化とエッジ検出』参照)。また、ごま塩ノイズ(salt-and-pepper noise)と呼ばれる、小さく、かつ極端な濃度差のノイズに対しては、非線形のメディアンフィルタ(median filter)が効果的です。
しかし、二値画像では、結果が中間調となるフィルタは使用できず、収縮(erosion)や膨張(dilation)などの数理形態学的処理が用いられます。
数理形態学的処理(mathematical morphology operation)
難しい名前が付いていますが、やっていることは簡単です。ここでは背景色(background color)を0(白)、前景色(foreground color)を1(黒)として話を進めます。処理には、基本として収縮(erosion、「腐食」の意味があり、周辺が錆びて減って行く意味です。)と膨張(dilation)があり、これらを順に行なうオープニング(opening、「収縮」→「膨張」)とクロージング(closing、「膨張」→「収縮」)があります。オープニングは、白い背景に点在する小さな黒いゴミを除去し、誤って連結している部分を切り開く(オープニング)効果があります。クロージングは、黒い画像に開いた白い穴を埋め、誤って切断されている部分を閉じる(クロージング)のに適しています。
収縮(erosion)と膨張(dilation)の具体的な方法
収縮は、注目する周辺のピクセル(例えば3 x 3)に、一個でも0(白)があれば注目するピクセルを0とし、膨張は、一個でも1(黒)があれば注目するピクセルを1とします。
周辺のピクセルは、3 x 3の範囲のすべて(注目するピクセル以外の8個)を用いる場合と、上下左右の4個のみを用いる場合とがあります。当然ながら、4個よりは8個を用いる方が、効果は大きくなります。
原則的には実行は一回限りですが、効果が不足する場合は、繰返して実行します。逆に、効果を緩和したい場合には、周辺のピクセルの1または0の個数の条件を2個以上に増やします。
プログラムの概要
プログラムは、下記から成っています。
- 選択ボックスで指定された原画像の読み込み
- 原画像の表示
- 原画像を一次元RGBデータに変換
- 一次元RGBデータにより、赤を抽出
- 抽出された二値画像を収縮し、結果を表示
- 収縮された二値画像を膨張し、結果を表示
なお、収縮処理と膨張処理は周辺のピクセルを参照しますので、処理の直前に画像の四辺を拡張しておきます。
プログラム
import java.applet.Applet; import java.awt.*; import java.awt.image.*; import java.awt.event.*; public class Red extends Applet implements ItemListener{ int WIDTH=320,HEIGHT=240,SIZE=76800; int X0=10,Y0=50,X1=340,Y1=310; Choice choice; String filename; String[] file={"camera029.jpg", //順光の画像 "camera036.jpg", //陰影の画像 "camera034.jpg", //逆光の画像 "camera019.jpg", //複雑な画像 "camera038.jpg"}; //夜間の画像 int[] rgb=new int[SIZE]; byte[][] pixels=new byte[HEIGHT][WIDTH]; byte[][] pixels_exp=new byte[HEIGHT+2][WIDTH+2]; public void init(){ //GUIの設定 setLayout(new FlowLayout(FlowLayout.LEFT)); choice=new Choice(); choice.add("順光の画像"); choice.add("陰影の画像"); choice.add("逆光の画像"); choice.add("複雑な画像"); choice.add("夜間の画像"); choice.addItemListener(this); add(choice); filename="camera029.jpg"; //ファイル名の初期設定 } //チョイスが変更になったときのメソッド public void itemStateChanged(ItemEvent ie){ filename=file[choice.getSelectedIndex()]; repaint(); } public void paint(Graphics gr){ int r,g,b; Color color; //画像ファイルを読み込み、Image画像imgにする Image img=readImageFile(filename); //Image画像imgを画面左上に表示する gr.drawImage(img,X0,Y0,this); //Image画像imgを一次元RGBデータrgbに変換する createRGBData(img,rgb); //画像の枠を描く gr.drawRect(X1,Y0,WIDTH,HEIGHT); gr.drawRect(X0,Y1,WIDTH,HEIGHT); gr.drawRect(X1,Y1,WIDTH,HEIGHT); // ---------------------- 赤の抽出 -------------------------- for(int j=0;j<HEIGHT;j++) for(int i=0;i<WIDTH;i++){ color=new Color(rgb[j*WIDTH+i]); r=color.getRed(); g=color.getGreen(); b=color.getBlue(); //赤を抽出して描画し、 //二次元ピクセルデータpixelsに書き込む if(r>1.75*g && r>1.75*b && r>32){ //赤の抽出条件 //画面右上に描画する gr.drawLine(X1+i,Y0+j,X1+i,Y0+j); pixels[j][i]=1; } else pixels[j][i]=0; } // ---------------------- 収縮処理 -------------------------- //二次元ピクセルデータpixelsを拡張してpixels_expにする expand2DPixelsData(pixels,pixels_exp); //二次元ピクセルデータpixels_expを //収縮(erosion)してpixelsにする erodePixelData(pixels_exp,pixels); //二次元ピクセルデータpixelsで図形を描画する gr.setColor(Color.black); for(int j=0;j<HEIGHT;j++) for(int i=0;i<WIDTH;i++) if(pixels[j][i]==1) //画面左下に描画する gr.drawLine(X0+i,Y1+j,X0+i,Y1+j); // ---------------------- 膨張処理 -------------------------- //二次元ピクセルデータpixelsを拡張してpixels_expにする expand2DPixelsData(pixels,pixels_exp); //二次元ピクセルデータpixels_expを //膨張(dilation)してpixelsにする dilatePixelData(pixels_exp,pixels); //二次元ピクセルデータpixelsで図形を描画する gr.setColor(Color.black); for(int j=0;j<HEIGHT;j++) for(int i=0;i<WIDTH;i++) if(pixels[j][i]==1) //画面右下に描画する gr.drawLine(X1+i,Y1+j,X1+i,Y1+j); } //画像ファイルを読み込みImageクラスの画像にするメソッド public Image readImageFile(String _filename){ Image img=getImage(getDocumentBase(),_filename); MediaTracker mtracker=new MediaTracker(this); mtracker.addImage(img,0); try{ mtracker.waitForAll(); }catch(Exception e){} return img; } //Imageクラスの画像を一次元RGBデータに変換するメソッド public void createRGBData(Image img, int[] rgb){ int width=img.getWidth(this); int height=img.getHeight(this); int size=width*height; PixelGrabber grabber=new PixelGrabber( img,0,0,width,height,rgb,0,width); try{ grabber.grabPixels(); }catch(InterruptedException e){} } //二次元ピクセルデータを拡張するメソッド public void expand2DPixelsData( byte[][] pixels, byte[][] pixels_exp){ //元の二次元ピクセルデータpixels[][]を、 //拡張後のpixels_exp[][]の中心に設定する for(int j=0;j<HEIGHT;j++) for(int i=0;i<WIDTH;i++) pixels_exp[j+1][i+1]=pixels[j][i]; //上辺と下辺をコピーする for(int i=0;i<WIDTH+2;i++){ pixels_exp[0][i]=pixels_exp[1][i]; pixels_exp[HEIGHT+1][i]=pixels_exp[HEIGHT][i]; } //左辺と右辺をコピーする for(int j=0;j<HEIGHT+2;j++){ pixels_exp[j][0]=pixels_exp[j][1]; pixels_exp[j][WIDTH+1]=pixels_exp[j][WIDTH]; } } //縮小(erosion)処理をするメソッド public void erodePixelData(byte[][] pixels_exp, byte[][] pixels){ int k,l; int n; for(int j=1;j<=HEIGHT;j++) for(int i=1;i<=WIDTH;i++){ //注目するピクセルが0の場合は処理不要 if(pixels_exp[j][i]==0) continue; n=0; for(k=-1;k<=1;k++) for(l=-1;l<=1;l++) if(pixels_exp[j+l][i+k]==0){ n=1; break; } //周辺ピクセルに0が一個以上あったので //注目ピクセルを0にする if(n>0) pixels[j-1][i-1]=0; } } //膨張(dilation)処理をするメソッド public void dilatePixelData(byte[][] pixels_exp, byte[][] pixels){ int k,l; int n; for(int j=1;j<=HEIGHT;j++) for(int i=1;i<=WIDTH;i++){ //注目するピクセルが1の場合は処理不要 if(pixels_exp[j][i]==1) continue; n=0; for(k=-1;k<=1;k++) for(l=-1;l<=1;l++) if(pixels_exp[j+l][i+k]==1){ n=1; break; } //周辺ピクセルに1が一個以上あったので //注目ピクセルを1にする if(n>0) pixels[j-1][i-1]=1; } } }