2006.9.18.
石立 喬

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

――原画像のヒストグラムを変換して新しい画像を描く――


 近傍のピクセル値との演算を行わない画像処理の一つとして、原画像のヒストグラムを求め、ヒストグラムに変更を加えて画像を補正する方法を説明する。一般の画像(リタッチ)ソフトには必ずと言ってよい程に含まれている機能である。
 原画像の明暗が弱すぎる(眠い画像、曇っている時、照明が暗い時など)場合や、明暗が強すぎる(コントラストが強い画像、カンカン照りの時、逆光の時など)場合は、全体を暗くしたり明るくしたりするだけではダメで、ヒストグラムを変更して画像を補正する方法が採られる。この方法は、「易しい画像処理(2)」で紹介した濃度変換に似ているが、自動的な補正が容易である特徴がある。ヒストグラムとは、RGB各色の濃度0〜255を横軸にとり、それぞれのピクセル数を縦軸にとったもので、実際に補正に使われるのは、RGB各値の平均値や、その最大値(明度、HSB系でのBrightnessに相当)が多い。
 ここでは、原画像の明暗が弱すぎる場合のヒストグラムの拡大と、原画像の明暗が強すぎる場合のヒストグラムの平坦化について紹介する。

カラー画像のヒストグラムの取り扱い方
 グレイスケール画像のヒストグラムは濃淡の一種類だけなので簡単であり、ヒストグラム補正の効果は非常に優れているが、カラー画像の場合には、RGB成分に分かれているので、どの成分のヒストグラムを用いるかが問題になる。画像ソフトでは、RGBの単純加算(または平均)が多く用いられるが、色相の変化は避けられない。
 他に、人間の視覚に近いLuminosity(またはLuminance、NTSC方式のY信号に相当)を用いるもの、HSB表示系のBrightness(明度、RGBの最大値)を用いるものがあるが、これらもRGBの分布次第では色相の歪みは避けられない。
 ここでは、Brightnessを使用した例を紹介する。

ヒストグラムの拡張(Histogram Stretch)
ヒストグラムの拡張は明度(Brightness)のヒストグラムを用いて、次のステップで行う。
 1)原画像のヒストグラム(RGBおよび明度の)を求める。
 2)明度ヒストグラムの濃度レベル5%をLebel1、95%をLebel2として値を求める
 3)上記で求めたLebel1が0に、Lebel2が255になるようにRGBの各ヒストグラムを線形的に変換する。
 4)変換したヒストグラムにより新しい画像を描く。

ヒストグラムの平坦化(Histogram Equalization)
 ヒストグラムの平坦化は、RGBの各ヒストグラムについて個別に行う。
1)原画像のヒストグラムを求める。
 2)原画像のRGB各濃度値と、補正後のRGB各濃度値の変換テーブルを作成する。
   ここでは原画像サイズを320×240と決めているので、このヒストグラムのデータの総合計は320 × 240 = 76800となる。濃度値は、RGBそれぞれ0〜255の256種類であるので、ヒストグラムを平均化すると言うことは、320×240を256で割って、各濃度値のデータ数を300個にすることである。したがって、濃度0から始まって、データ(ピクセル)数の累計が300を超える濃度を補正後の画像の濃度0とする。同様に原画像のデータ数の累積がさらに300を超えるたびに新しい濃度値を与えて行く。原画像で同一濃度のデータ数が300を超える場合は、新濃度を飛ばして設定する(たとえば、濃度53から濃度54 に1だけ増やす代わりに、53から56に3だけ増やす)。ただし、実際には300の代わりに301を使用する(300にすると、稀に濃度256 が出現する恐れがある)。
 3)以上の方法で作成した変換テーブルで、原画像のRGB値から新画像のRGB値を求め、描画する。

プログラム(ヒストグラムの拡張)
 グローバル変数を下記のように宣言する。配列の宣言と同時に添え字の数を設定することはできないので、後述のForm1_Loadを使用する。

private:
   /// <summary>
   /// 必要なデザイナ変数です。
   ///
</summary>
   
   System::ComponentModel::Container ^components;

   static int HEIGHT=240,WIDTH=320,SIZE=76800;

   array<int>^ hist_R;
   array<int>^ hist_G;
   array<int>^ hist_B;
   array<int>^ hist_V;

 プログラムが呼び出されると一度だけ実行されるForm1_Lordで、上記グローバル変数を設定する。

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

   hist_R=gcnew array<int>(256);
   hist_G=gcnew array<int>(256);
   hist_B=gcnew array<int>(256);
   hist_V=gcnew array<int>(256);

}

 フォームが画面上に現れるときに実行されるForm1_Paintにメインのプログラムと、自作のメソッドを記述する。

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

   int X0=10,Y0=10;
   int X1=350,Y1=260;

   int i,j;
   int level1,level2;
   int maximum;
   int ratio;
   int r,g,b,r1,g1,b1;
   Bitmap^ bmap1=gcnew Bitmap(WIDTH,HEIGHT);
   Color color;

   Graphics^ gr=e->Graphics;

   //原画像ファイルを読み込んでBitmap画像bmapを得る
   Bitmap^ bmap=gcnew Bitmap("A:/sample.jpg");

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

   //ヒストグラム表示の枠を描く
   for(i=0;i<4;i++)
      gr->DrawRectangle(Pens::Black,X1,Y0+60*i,256,60);

   //原画像のヒストグラムを採る
   createHistogram(bmap);

   //ヒストグラムの最大値を求める
   maximum=0;
   for(i=0;i<256;i++)
      if(hist_V[i]>maximum) maximum=hist_V[i];
   ratio=maximum/50;

   //ヒストグラムを表示する
   for(i=0;i<256;i++){
      gr->DrawLine(Pens::Red,X1+i,Y0+60,X1+i,Y0+60-hist_R[i]/ratio);
      gr->DrawLine(Pens::Green,X1+i,Y0+120,X1+i,Y0+120-hist_G[i]/ratio);
      gr->DrawLine(Pens::Blue,X1+i,Y0+180,X1+i,Y0+180-hist_B[i]/ratio);
      gr->DrawLine(Pens::Gray,X1+i,Y0+240,X1+i,Y0+240-hist_V[i]/ratio);
   }

   //ヒストグラムの5%を求めて表示する
   int integration=0;
   i=0;
   do{
      integration+=hist_V[i];
      i++;
   }while(integration<SIZE/20);
   level1=i;
   gr->DrawLine(Pens::Black,X1+level1,Y0,X1+level1,Y0+240);

   //ヒストグラムの95%を求めて表示する
   integration=0;
   i=255;
   do{
      integration+=hist_V[i];
      i--;
   }while(integration<SIZE/20);
   level2=i;
   gr->DrawLine(Pens::Black,X1+level2,Y0,X1+level2,Y0+240);

   //ヒストグラムの補正(拡張)
   for(j=0;j<HEIGHT;j++)
      for(i=0;i<WIDTH;i++){
         color=bmap->GetPixel(i,j);
         r=color.R;
         g=color.G;
         b=color.B;
         if(r<level1) r1=0;
         else if(r>level2) r1=255;
         else r1=(int)(255.0/(level2-level1)*(r-level1));
         if(g<level1) g1=0;
         else if(g>level2) g1=255;
         else g1=(int)(255.0/(level2-level1)*(g-level1));
         if(b<level1) b1=0;
         else if(b>level2) b1=255;
         else b1=(int)(255.0/(level2-level1)*(b-level1));
         color=Color::FromArgb(r1,g1,b1);
         bmap1->SetPixel(i,j,color);
      }

   //ヒストグラム補正画像を左下に表示する
   gr->DrawImage(bmap1,X0,Y1,WIDTH,HEIGHT);

   //ヒストグラム表示の枠を描く
   for(i=0;i<4;i++)
      gr->DrawRectangle(Pens::Black,X1,Y1+60*i,256,60);

   //補正画像のヒストグラムを採る
   createHistogram(bmap1);

   //ヒストグラムを表示する
   for(i=0;i<256;i++){
      gr->DrawLine(Pens::Red,X1+i,Y1+60,X1+i,Y1+60-hist_R[i]/ratio);
      gr->DrawLine(Pens::Green,X1+i,Y1+120,X1+i,Y1+120-hist_G[i]/ratio);
      gr->DrawLine(Pens::Blue,X1+i,Y1+180,X1+i,Y1+180-hist_B[i]/ratio);
      gr->DrawLine(Pens::Gray,X1+i,Y1+240,X1+i,Y1+240-hist_V[i]/ratio);
   }

}

private: Void createHistogram(Bitmap^ bmap){

   Color color;
   int i,j,r,g,b;

   for(i=0;i<256;i++){
      hist_R[i]=0;
      hist_G[i]=0;
      hist_B[i]=0;
   }

   for(j=0;j<HEIGHT;j++)
      for(i=0;i<WIDTH;i++){
         color=bmap->GetPixel(i,j);
         r=color.R;
         g=color.G;
         b=color.B;

         hist_R[r]++;
         hist_G[g]++;
         hist_B[b]++;
      }

   for(i=0;i<256;i++)
      hist_V[i]=Math::Max(Math::Max(hist_R[i],hist_G[i]),hist_B[i]);

}

プログラム(ヒストグラムの平坦化)
 ヒストグラムの拡張のためのプログラムから変更した部分のみを示す。

Form1_Loadでグローバル変数を初期化する。

array<int>^ table_R=gcnew array<int>(256);
array<int>^ table_G=gcnew array<int>(256);
array<int>^ table_B=gcnew array<int>(256);

 Form1_Paintへの記述を変更した部分を示す。

//平坦化のための変換テーブルの作成
int integration=0;
j=0;
for(i=0;i<256;i++){
   integration+=hist_R[i];
   j+=integration/301;
   integration %=301;
   table_R[i]=j;
}
integration=0;
j=0;
for(i=0;i<256;i++){
   integration+=hist_G[i];
   j+=integration/301;
   integration %=301;
   table_G[i]=j;
}
integration=0;
j=0;
for(i=0;i<256;i++){
   integration+=hist_B[i];
   j+=integration/301;
   integration %=301;
   table_B[i]=j;
}

//ヒストグラムの補正(平坦化)
for(j=0;j<HEIGHT;j++)
   for(i=0;i<WIDTH;i++){
      color=bmap->GetPixel(i,j);
      r=color.R;
      g=color.G;
      b=color.B;
      r1=table_R[r];
      g1=table_G[g];
      b1=table_B[b];
      color=Color::FromArgb(r1,g1,b1);
      bmap1->SetPixel(i,j,color);
   }


得られた結果(ヒストグラムの拡張)
 図1で、左上は原画像、右上は原画像のヒストグラム、左下がヒストグラムを拡張した新しい画像、右下は新しい画像のヒストグラムである。ヒストグラムは、上からR、G、B、明度の順に示されており、右上のヒストグラムには、ピクセル数の5%(左の縦線)と95%(右の縦線)が示されている。原画像は、ヒストグラムの幅が狭く、明暗がはっきりしないことが解る。それに対して、変換後のヒストグラムは広い範囲に分布しており、左下の新しい画像を見ても、コントラストが上がっていることが分かる。色相の微妙な変化は止むを得ない。



図1 ヒストグラムの拡張の実行画面


得られた結果(ヒストグラムの平坦化)
 図2で、左上は原画像、右上は原画像のヒストグラム、左下はヒストグラムを平坦化した新しい画像、右下は平坦化された画像のヒストグラムである。 原画像は、日陰の場所と日照りの場所の明暗が著しく、右上のヒストグラムでもわかる。右下のヒストグラムで、ピクセル数の大きい(縦軸方向に大きい)濃度は横軸方向の密度が低く間隙が開いており、ピクセル数の小さい濃度は密度が高く、全体としてヒストグラムの平坦化が行われていることがわかる。



図2 ヒストグラムの平坦化の実行画面