はじめに
画像処理の応用の一つに、車や人物といった物体の検出があります。まだ、十分完成した技術とはいえませんが、車の場合は、車輪のような丸いものが一定の間隔で並んでいると車らしいと判断し、人物の場合は、丸みを帯びた白いものの真中付近に二つ並んで黒いものがあり、さらにその下の中央部に、横に長いものがあれば人物らしいと判断します。
特に、人物の検出は、防犯やセキュリティ管理の上で重要視され、研究が盛んです。最近、カメラの被写体の中から人物を探し、そこに自動的に焦点をあわせるカメラも出現しました。
画像の中から、上記のような特定のパターンを探し出すには、「テンプレートマッチング法」が良く用いられます。テンプレートとは型紙のことで、それを画像上で移動させながら比較して行くやり方です。ただし、単純な方法では、テンプレートに対して傾斜を持っている画像や、相似形であるが大きさが異なる画像は検出できません。それらに対応させるには、より面倒な処理が必要になり、時間がかかります。
ここでは、テンプレートマッチングの最も簡単な方法を紹介しますが、検出性能は低く、あまり実用的ではありません。技法の理解と遊びが目的と考えてください。
対象読者
画像処理に関心があり、画像の二値化、マスク処理などを学習したい人。
必要な環境
J2SE5.0を使っていますが、それより若干旧いバージョンでも大丈夫です。CPUパワーは、大きい方がストレスを感じさせません。
プログラムの概要
図1に示すように、このプログラムは次の部分から成っています。
- 画像を読み込む
readImageFile
メソッド - 画像をグレイ化する
changeToGray
メソッド - グレイ画像を二値化する
changeToBinary
メソッド - 二つの二値画像を比較する
searchMatching
メソッド
これに対して、被探索画像としての「Faces.jpg」と、テンプレート画像「Temp.gif」を用意し、アプレットを呼び出すHTMLファイルと同じフォルダ(ディレクトリ)に入れておきます。テンプレート画像をGIFにしたのは、JPEGだと非可逆圧縮のために色ににじみが発生し、正確な比較ができないからです。
searchMatching
メソッドに与えるx0
とy0
のパラメータは、探索結果を表示する画像の左上の座標です。ここでは、被探索二値画像の座標を選んでいます。threshold
パラメータは、これを大きくすると条件が厳しくなり、なかなか合致しません。逆に小さいと、顔以外のものを顔として判定するケースが増えてきます。
上記1.から3.までをinit()
メソッドから呼び出し、テンプレートマッチングの準備をします。4.はpaint()
メソッドから呼び出し、顔が見つかった場合に赤枠で表示します。ただし、この方法では、処理の終了後に自動的に再描画が始まり、赤枠の表示が二回行われる不自然さが残ります。
グレイ画像化の方法
ファイルから読み込んだ被探索画像データimg_src
は、PixelGrabber
によってピクセル数(size
)と同じ個数の一次元配列rgb_src[i]
に変換されます。これは、「0xAARRGGBB」で表すことができる32ビットのint
です。これからR、G、Bそれぞれの値を取り出すには、
color=new Color(rgb_src[i]);
r=color.getRed();
g=color.getGreen();
b=color.getBlue();
を用います。「AA」は透明度で、デフォルトではff
(不透明)に設定されています。
従来は、上記の代りに、
r=(rgb_src[i] >> 16) & 0xff; g=(rgb_src[i] >> 8) & 0xff; b=rgb_src[i] & 0xff;
などを用いるのが一般的でしたが、可読性のためにも、あまり低レベルの処理は避けた方が良いと思います。
分解されたr
、g
、b
値は、
d=(r+3+g*6+b)/10;
によって、明るさd
に変換されます。この式は、テレビのNTSC方式における輝度信号Y
の求め方、
Y=0.30 x R + 0.59 x G + 0.11 x B
を参考にしたものです。r
、g
、b
値の単なる平均を取る場合もありますが、緑成分が、人間の目には最も明るく感じる事実は利用したいものです。
明るさを表すd
から、「0xAARRGGBB」の形のrgb_gray[i]
を得るには、
color=new Color(d,d,d);
rgb_gray[i]=color.getRGB();
を用います。
従来は、上記の代りに、
rgb_gray[i]=0xff000000 | d<<16 | d<<8 |d;
rgb_gray[i]=0xff000000 | 0x00010101*d;
などが好んで用いられましたが、前述の理由で、避けた方が良いでしょう。
最後に、描画のために、MemoryImageSource
を用いて、配列rgb_gray[i]
を画像データimg_gray
に変換します。
二値画像化の方法
グレイ画像データimg_src
は、PixelGrabber
によって、再び一次元配列rgb_gray[i]
に戻されます。実は、この作業は二度手間なのですが、メソッドの独立性を高め、インターフェイスを統一するために実行しています。
前述したように、
color=new Color(rgb_gray[i]);
d=color.getBlue();
で明るさを示す成分d
を取得します。グレイ画像では、r
、g
、b
のどの成分も同じですから、d=color.getRed();
などでも同じ結果が得られます。
次に、二値化のための閾値threshold
を求めます。まず、rgb_gray[i]
の全ピクセルをスキャンして、明るさdのヒストグラム(histogram[0]
からhistogram[255]
まで)を取ります。次に、histogram[0]
から始めてピクセル数の累計を求め、それが全ピクセル数(size
)の半分になった時の明るさをthreshold
と決めます。すなわち、全ピクセルの50%が白、50%が黒になるようにします。
threshold
が決まれば、rgb_gray[i]
から得た明るさd
がthreshold
より大きいときはd=255
(白)、小さいときはd=0
(黒)に振り分けて二値化します。
テンプレートマッチングの方法
図2は、テンプレートマッチングに使用する被探索画像とテンプレート画像の座標関係を示したものです。
被探索画像上を、テンプレート画像が、y
方向とx
方向にスキャンしますが、このx
とy
の位置がテンプレート画像の左上(原点)に来ます。したがって、スキャンの範囲は、被探索画面の幅や高さから、テンプレート画像の幅や高さを引いたものです。これで、被探索画面全体を調べることができます(一部しか見えていない顔は検出できません)。
図3は、searchMatching
メソッドを説明する流れ図です。まず、準備作業として、被検索画像の二次元ピクセルデータpixels_bin[y][x]
とテンプレート画像の二次元ピクセルデータpixels_temp[j][i]
を用意します。ここで、配列をpixels_bin[x][y]
でなく、pixels_bin[y][x]
としたのは、格納されているメモリ位置に連続性を持たせて、CPU内のキャッシュを有効に利用し、高速化するためです。
被探索画像上を、テンプレート画像が、y
方向とx
方向にスキャンしますが、このx
とy
の位置がテンプレート画像の左上(原点)に来るようにして、さらにテンプレート画像上をj
方向とi
方向に調べます。すなわち、pixels_temp[j][i]
とpixels_bin[y+j][x+i]
を比較します。
一致した個数をカウントするために、あらかじめs=0
に設定します。
pixels_temp[j][i]
が「-1」の場合は、その場所が無視すべき点(Don't care)であるので、比較をしないで次に進みます。
i>t_width/3
で、かつi<t_width*2/3
の場合は、顔の縦方向の中央部ですので、カウントの重みを4倍にしています。
テンプレート画像上の全てのピクセル(無視すべき点を除く)を比較し終わったら、一致したカウント数を調べます。もしs>threshold
が成立すれば、顔であると判定し、drawRect
でテンプレート画像と同じ大きさの矩形を描きます。threshold
の値には、試行錯誤の結果、「1300」を用いています。
プログラム
import java.awt.*; import java.awt.image.*; import javax.swing.*; public class Template extends JApplet{ Image img_src,img_gray,img_bin,img_temp; public void init(){ //画像Faces.jpgをimg_srcとして読み込む img_src=readImageFile("Faces.jpg"); //img_srcをグレイ化してimg_grayを作る img_gray=changeToGray(img_src); //img_grayを二値化してimg_binを作る img_bin=changeToBinary(img_gray); //テンプレート画像Temp.gifをimg_tempとして読み込む img_temp=readImageFile("Temp.gif"); } //画像ファイルを読み込み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クラスのカラー画像をImageクラスのグレイ画像にするメソッド public Image changeToGray(Image img_src){ int i,r,g,b,d; Color color; int width=img_src.getWidth(this); int height=img_src.getHeight(this); int size=width*height; int[] rgb_src=new int[size]; int[] rgb_gray=new int[size]; PixelGrabber grabber= new PixelGrabber(img_src,0,0,width,height,rgb_src,0,width); try{ grabber.grabPixels(); //画像imgを配列rgb_src[]に読み込む }catch(InterruptedException e){} //カラー画像をグレイ化する for(i=0;i<size;i++){ color=new Color(rgb_src[i]); r=color.getRed(); //赤の成分を取り出す g=color.getGreen(); //緑の成分を取り出す b=color.getBlue(); //青の成分を取り出す d=(g*6+r*3+b)/10; //グレイの成分を作る(NTSC方式準拠) color=new Color(d,d,d); rgb_gray[i]=color.getRGB(); } Image img_gray=createImage( new MemoryImageSource(width,height,rgb_gray,0,width)); return img_gray; } //Imageクラスのグレイ画像をImageクラスの二値画像にするメソッド public Image changeToBinary(Image img_gray){ int i,r,g,b,d,s; Color color; int width=img_gray.getWidth(this); int height=img_gray.getHeight(this); int size=width*height; int[] rgb_gray=new int[size]; int[] rgb_bin=new int[size]; //画像imgを配列rgb_gray[]に読み込む PixelGrabber grabber=new PixelGrabber(img_gray,0,0, width,height,rgb_gray,0,width); try{ grabber.grabPixels(); }catch(InterruptedException e){} //ヒストグラム取得のためのhistogram[256]を生成し、ゼロに初期化 int[] histogram=new int[256]; for(i=0;i<256;i++) histogram[i]=0; //ヒストグラムを取得する for(i=0;i<size;i++){ color=new Color(rgb_gray[i]); d=color.getBlue(); //グレイ画像なので、getRed()などでも良い histogram[d]++; } //頻度数の中間値を求め、thresholdとする i=0; s=0; while(s<size/2){ s+=histogram[i++]; } int threshold=i-1; //-1で、最後に実行したi++の処理を元に戻す //グレイ画像のrgb_gray[]を二値化し、 //ニ値画像のrgb_bin[]に変換する for(i=0;i<size;i++){ color=new Color(rgb_gray[i]); d=color.getBlue(); //グレイ画像なので、getRed()などでも良い if(d>threshold) d=255; //白 else d=0; //黒 color=new Color(d,d,d); rgb_bin[i]=color.getRGB(); } Image img_bin=createImage( new MemoryImageSource(width,height,rgb_bin,0,width)); return img_bin; } //テンプレートマッチングのメソッド public void searchMatching(Graphics g,int x0,int y0, Image img_bin,Image img_temp,int threshold){ int x,y,i,j,s; Color color; // 被探索二値画像 img_bin の処理の準備 int b_width=img_bin.getWidth(this); int b_height=img_bin.getHeight(this); int b_size=b_width*b_height; // 被探索二値画像 img_bin を一次元配列 rgb_bin[]に変換する int[] rgb_bin=new int[b_size]; PixelGrabber grabber_bin=new PixelGrabber( img_bin,0,0,b_width,b_height,rgb_bin,0,b_width); try{ grabber_bin.grabPixels(); }catch(InterruptedException e){} // 一次元配列 rgb_bin[]を // 二次元二値配列 pixels_bin[][]に変換する byte[][] pixels_bin=new byte[b_height][b_width]; for(y=0;y<b_height;y++) for(x=0;x<b_width;x++){ color=new Color(rgb_bin[y*b_width+x]); if(color.getRed()==255) pixels_bin[y][x]=1; //白 else pixels_bin[y][x]=0; //黒 } // テンプレート画像 img_temp の処理の準備 int t_width=img_temp.getWidth(this); int t_height=img_temp.getHeight(this); int t_size=t_width*t_height; // テンプレート画像 img_temp をrgb_temp[]に変換する int[] rgb_temp=new int[t_size]; PixelGrabber grabber_temp=new PixelGrabber( img_temp,0,0,t_width,t_height,rgb_temp,0,t_width); try{ grabber_temp.grabPixels(); }catch(InterruptedException e){} // 一次元配列 rgb_temp[]を // 二次元三値配列 pixels_temp[][]に変換する byte[][] pixels_temp=new byte[t_height][t_width]; for(y=0;y<t_height;y++) for(x=0;x<t_width;x++){ color=new Color(rgb_temp[y*t_width+x]); //白 if(color.getRed()==255) pixels_temp[y][x]=1; //緑(Don't care) else if(color.getGreen()==255) pixels_temp[y][x]=-1; //黒 else pixels_temp[y][x]=0; } //マッチング個所を探索し、赤い枠で表示する g.setColor(Color.red); for(y=0;y<b_height-t_height;y++) for(x=0;x<b_width-t_width;x++){ s=0; for(j=0;j<t_height;j++) for(i=0;i<t_width;i++){ //比較しない if(pixels_temp[j][i]==-1) continue; //一致した if(pixels_temp[j][i]==pixels_bin[y+j][x+i]){ //縦方向の中央部 if(i>t_width/3 && i<t_width*2/3) s+=4; else s++; } } if(s>threshold) g.drawRect(x0+x,y0+y,t_width,t_height); } } public void paint(Graphics g){ //被探索画像img_srcを描画 g.drawImage(img_src,10,0,img_src.getWidth(this), img_src.getHeight(this),this); //グレイ画像img_grayを描画 g.drawImage(img_gray,340,0,img_gray.getWidth(this), img_gray.getHeight(this),this); //二値画像img_binを描画 g.drawImage(img_bin,10,260,img_bin.getWidth(this), img_bin.getHeight(this),this); //テンプレート画像img_tempを描画 g.drawImage(img_temp,340,260,img_temp.getWidth(this), img_temp.getHeight(this),this); //テンプレートマッチングを実施、結果を二値画像 img_bin 上に表示 searchMatching(g,10,260,img_bin,img_temp,1300); } }
得られた画面
図4で、左上は被探索原画像、右上はグレイ画像、左下は二値画像の上に、顔の検出結果が赤枠で示されています。その右は、参考の為に表示したテンプレート画像です。
斜めになってなく、正面を向いていて、影の少ない顔は大体検出されていますが、顔でないものも顔と誤って認識されています。
まとめ
ここでは、下記のような画像処理の基本を紹介しました。
- 画像ファイルの読み込み
- カラー画像のグレイ画像化
- グレイ画像の二値化
- テンプレートマッチング法による画像の比較
テンプレートマッチング法による顔画像の検出は、ここで紹介したような単純な方法では、実用的とはいえませんが、上記の基本操作の知識は、きっと画像処理一般に役立つことと思います。
参考資料
この記事は、筆者のホームページVisual C++を用いた易しい画像処理(8)―テンプレートマッチング法を用いた画像の検索―(ただし、現在は Visual C++ 2005 Express Edition 版に改定)をJava言語に書き直し、分かりやすく加筆したものです。
特に参考資料はありませんが、Java言語による一般的な画像処理では、以下の資料があります。
- 『楽しく学ぶJavaではじめる画像処理プログラミング』 杉山三樹雄 著、ディーアート、2002年12月