プログラムの概要
画面描画時の主な動作(paint()
の内容)は、次の通りです。
- 選択ボックスの指定に従って、画像ファイルを読み込み、原画像とする。
- 原画像を表示する。
- 原画像をR、G、Bに分解しL(輝度、luminosity)を計算する。
- 原画像のR、G、B、Lの各ヒストグラムを求め、表示する。
- 上記Lヒストグラムの統計量(
min
、mid
、max
)を求めて、線と数値で表示する。
- ヒストグラム拡張の場合
- 原画像の
min
が0に、max
が255になるように、比例関係式を用いてR、G、B、L各ヒストグラムを拡張する。 - 比例関係式を用いて原画像のR、G、Bを補正する。
- 比例関係式を用いて統計量を求める。
- ヒストグラム平坦化の場合
- 原画像のLヒストグラムから換算表(Look Up Table)を作成し、それを用いてR、G、B、L各ヒストグラムを平坦化する。
- 換算表を用いて原画像のR、G、Bを補正する。
- 換算表を用いて統計量を求める。
- 補正後のR、G、Bから補正画像を生成し、補正画像を表示する。
- 補正後のR、G、B、L各ヒストグラムを表示する。
- 補正後のLヒストグラムの統計量(
min
、mid
、max
)を表示する。
プログラム
import java.applet.Applet; import java.awt.*; import java.awt.image.*; import java.awt.event.*; public class Hist extends Applet implements ItemListener,ActionListener{ // ……中略…… public void init(){ // ……中略…… } // ----------------------- GUI関係メソッド ------------------------ //チョイスが変更になった場合のメソッド public void itemStateChanged(ItemEvent ie){ // ……中略…… } //ボタンをクリックしたときのメソッド public void actionPerformed(ActionEvent ae){ // ……中略…… } // ------------------------- 共通メソッド ------------------------- //画像ファイルを読み込みImage画像にするメソッド public Image readImageFile(String filename){ // ……中略…… } //Image画像をRGBL値に分解するメソッド private void getRGBLFromImage(Image img, int[] red, int[] green, int[] blue, int[] lum){ Color color; int[] rgb=new int[SIZE]; PixelGrabber grabber= new PixelGrabber(img,0,0,WIDTH,HEIGHT,rgb,0,WIDTH); try{ grabber.grabPixels(); } catch(InterruptedException e){} for(int i=0;i<SIZE;i++){ color=new Color(rgb[i]); red[i]=color.getRed(); green[i]=color.getGreen(); blue[i]=color.getBlue(); lum[i]=(3*red[i]+6*green[i]+blue[i])/10; //NTSC方式に近似 } } //RGBL値のヒストグラムを個別にとるメソッド private void createHistogram(int[] rgb, int[] hist){ for(int i=0;i<LEVEL;i++) hist[i]=0; for(int i=0;i<SIZE;i++) hist[rgb[i]]++; } //ヒストグラムデータを描画するメソッド private void drawHistogram(int x, int y, int[] red_hist, int[] green_hist, int[] blue_hist, int[] lum_hist){ // ……中略…… } //ヒストグラムの統計量(min、mid、max)を求めるメソッド private void getStatisticsFromHistogram( int[] hist, int[] levels){ int percent1=SIZE/100; int percent50=SIZE/2; int percent99=SIZE/100*99; int i=0; int sum=0; do{ sum+=hist[i++]; } while(sum<percent1); levels[0]=i-1; //min do{ sum+=hist[i++]; } while(sum<percent50); levels[1]=i-1; //mid do{ sum+=hist[i++]; } while(sum<percent99); levels[2]=i-1; //max } //ヒストグラムの統計量(min、max、mid)を線と数値で表示するメソッド private void drawStatistics(int x, int y, // ……中略…… } //R、G、B成分からImage画像を生成するメソッド private Image createImageFromRGB( int[] red, int[] green, int[] blue){ // ……中略…… } // ----------------- ヒストグラム拡張関係メソッド ----------------- //ヒストグラムを拡張するメソッド private void spreadHistogram( int min, int max, int[] src_hist, int[] dest_hist){ int i,j; double ratio=255.0/(max-min); for(i=0;i<LEVEL;i++) dest_hist[i]=0; for(i=0;i<LEVEL;i++){ if(i<min) dest_hist[0]+=src_hist[i]; else if(i>max) dest_hist[LEVEL-1]+=src_hist[i]; else{ j=(int)((i-min)*ratio); dest_hist[j]=src_hist[i]; } } } //拡張ヒストグラムに応じてRGB成分を変換するメソッド private void spreadRGB(int min, int max, int[] src, int[] dest){ double ratio=255.0/(max-min); for(int i=0;i<SIZE;i++){ if(src[i]<min) dest[i]=0; else if(src[i]>max) dest[i]=255; else dest[i]=(int)((src[i]-min)*ratio); } } // ---------------- ヒストグラム平坦化関係メソッド ---------------- //原画像のヒストグラムから変換表(LookUpTable)を作成するメソッド private void generateLookUpTable(int[] hist, int[] table){ int i,j=0; int sum=0; int average=SIZE/LEVEL+1; for(i=0;i<LEVEL;i++){ sum+=hist[i]; j+=sum/average; sum %=average; table[i]=j; } } //ヒストグラムを平坦化するメソッド private void equalizeHistogram( int[] table, int[] src_hist, int[] dest_hist){ for(int i=0;i<LEVEL;i++) dest_hist[i]=0; for(int i=0;i<LEVEL;i++) dest_hist[table[i]]+=src_hist[i]; } //平坦化ヒストグラムに応じてRGB成分を変換するメソッド private void equalizeRGB(int[] table, int[] src, int[] dest){ for(int i=0;i<SIZE;i++) dest[i]=table[src[i]]; } public void paint(Graphics g){ String proccess=null; // ---------------------- 原画像の処理 ------------------------- //画像ファイルを読み込む img_src=readImageFile(filename); //原画像を左上に表示する g.drawImage(img_src,X0,Y0,null); //Image画像をR,G,B,Lに分解する getRGBLFromImage(img_src,src_red,src_green,src_blue,src_lum); //R,G,B,Lそれぞれのヒストグラムを求める createHistogram(src_red,src_red_hist); createHistogram(src_green,src_green_hist); createHistogram(src_blue,src_blue_hist); createHistogram(src_lum,src_lum_hist); //原画像のヒストグラムを右上に表示する drawHistogram(X1,Y0, src_red_hist,src_green_hist,src_blue_hist,src_lum_hist); int[] mmm_levels=new int[3]; //ヒストグラムのmin、max、midを求める getStatisticsFromHistogram(src_lum_hist,mmm_levels); min_level=mmm_levels[0]; mid_level=mmm_levels[1]; max_level=mmm_levels[2]; //ヒストグラムのmin、max、midを表示する drawStatistics(X1,Y0,min_level,mid_level,max_level); g.drawString("原画像",X1+270,Y0+20); // ------------------ ヒストグラム拡張の場合 ------------------- if(!equal_flag){ //ヒストグラムを拡大する spreadHistogram( min_level,max_level,src_red_hist,dest_red_hist); spreadHistogram( min_level,max_level,src_green_hist,dest_green_hist); spreadHistogram( min_level,max_level,src_blue_hist,dest_blue_hist); spreadHistogram( min_level,max_level,src_lum_hist,dest_lum_hist); //拡大ヒストグラムに応じてRGB成分を変換する spreadRGB(min_level,max_level,src_red,dest_red); spreadRGB(min_level,max_level,src_green,dest_green); spreadRGB(min_level,max_level,src_blue,dest_blue); //補正ヒストグラムの統計量を求める dest_min_level=0; dest_mid_level= (int)((mid_level-min_level)*255.0/(max_level-min_level)); dest_max_level=255; //ヒストグラム拡大結果であることを表示する proccess="拡張結果"; } // ---------------- ヒストグラム平坦化の場合 ------------------ if(equal_flag){ //LookUpTableを生成する generateLookUpTable(src_lum_hist,table); //ヒストグラム平坦化を行う equalizeHistogram(table,src_red_hist,dest_red_hist); equalizeHistogram(table,src_green_hist,dest_green_hist); equalizeHistogram(table,src_blue_hist,dest_blue_hist); equalizeHistogram(table,src_lum_hist,dest_lum_hist); //平坦化ヒストグラムに応じてRGB成分を変換する equalizeRGB(table,src_red,dest_red); equalizeRGB(table,src_green,dest_green); equalizeRGB(table,src_blue,dest_blue); //補正ヒストグラムの統計量を求める dest_min_level=table[min_level]; dest_mid_level=table[mid_level]; dest_max_level=table[max_level]; //ヒストグラム平坦化結果であることを表示する proccess="平坦化結果"; } // ----------- 共通(補正画像とヒストグラムの表示)------------- //R,G,B成分からImage画像を生成する img_dest=createImageFromRGB(dest_red,dest_green,dest_blue); //補正画像を左下に表示する g.drawImage(img_dest,X0,Y1,null); //補正画像のヒストグラムを右下に表示する drawHistogram(X1,Y1, dest_red_hist,dest_green_hist,dest_blue_hist,dest_lum_hist); //ヒストグラムの補正方法を表示する g.drawString(proccess,X1+270,Y1+20); //ヒストグラムの統計量(min、max、mid)を表示する drawStatistics(X1,Y1, dest_min_level,dest_mid_level,dest_max_level); } }
プログラムの使い方
アプレットを実行すると、上方右側に原画像選択用のドロップダウン選択ボックスと、補正方式切り替え用のボタンが表示されます。その下には、原画像とそのヒストグラム、レベルの最小値、中央値、最大値が示され、さらに下には、補正画像とそのヒストグラム、レベルの最小値、中央値、最大値が示されます。
起動時には、補正方式が「ヒストグラム拡張」に設定されており、ボタンには[→ヒストグラム平坦化]と表示されています。これをクリックすると、「ヒストグラム平坦化」に変わります。原画像は、デフォルトで「暗い画像」になっていて、「明るい画像」「明暗の画像」に切り替えることができます。
プログラムの実行結果
図4は実行結果の一例を示したもので、原画像には「明暗の画像」が選ばれています。この場合には、「ヒストグラム平坦化」が適しています。原画像と比べて、ヒストグラムが広がっていて、補正画像は、暗い部分が明るくなり、明るい部分は、逆に少し暗くなっていることが分かります。逆光の写真の補正に適しています。
まとめ
ヒストグラムを用いたカラー画像の補正は、色相のズレが生じるとされています。輝度データを基にして自動的にヒストグラム補正を行い、カラー情報は無視しましたが、色彩が強調された特殊な画像を除き、一般的な自然画像に対しては、十分使用できる結果が得られました。
参考資料
この記事は、筆者のウエブサイト『Visual C++ 6.0を用いた易しい画像処理(4)-原画像のヒストグラムを求め、ヒストグラムを変換して新しい画像を描く-(ただし、現在は Visual C++ 2005 EXpress Edition 版に改定)』を全面的に改良し、Java言語に書き直して、分かりやすく加筆したものです。
市販ソフトによるヒストグラムの補正方法を解説した書籍で比較的詳しいのは、次の2冊です。
- 『早川廣行のPhotoshop CSプロフェッショナル講座 基本編』 早川廣行 著、毎日コミュニケーションズ、2004年8月
- 『Adobe Photoshop CS2パーフェクトマスター』 KUMIKO 著、秀和システム、2005年9月