2010.12.20.
石立 喬

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

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


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

カラー画像のヒストグラムの取り扱い方
 グレイスケール画像のヒストグラムは濃淡の一種類だけなので簡単であり、ヒストグラム補正の効果は非常に優れているが、カラー画像の場合には、RGB成分に分かれているので、どの成分のヒストグラムを用いるかが問題になる。ここでは、「易しい画像処理(3)」で紹介したHSV表示系のValue(明度、RGBの最大値)を用いるが、これもRGBの分布次第では色相の歪みは避けられない。

ヒストグラムの拡張(Histogram Expansion)
 ヒストグラムの拡張は明度(Value)のヒストグラムを用いて、次のステップで行う。
 1)原画像のヒストグラム(RGBおよび明度Vの)を求める。
 2)明度ヒストグラムの濃度レベル5%(一例)をLebel_low、濃度レベル95%(一例)をLebel_highとして、これらの値を求める。
 3)上記で求めたLebel_lowが新しい濃度レベルの0に、Lebel_highが255になるように、下記により新しいRGBの各ヒストグラムを線形的に変換する。
        補正後の濃度=(原画像の濃度−level_low)/(level_high−level_low)x255
 4)変換したヒストグラムにより新しい画像を描く。

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

プログラム(ヒストグラムの拡張)

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

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

   int X0=10,Y0=10;
   int X1=340,Y1=260;
   int i,j;
   int maximum;
   int scale;
   int sum;
   Byte r,g,b,r1,g1,b1;
   Bitmap^ bmap_dst=gcnew Bitmap(WIDTH,HEIGHT);
   Color color_src,color_dst;
   //ヒストグラムの配列を用意する
   array<int>^ hist_R=gcnew array<int>(256);
   array<int>^ hist_G=gcnew array<int>(256);
   array<int>^ hist_B=gcnew array<int>(256);
   array<int>^ hist_V=gcnew array<int>(256);

   Graphics^ gr=e->Graphics;

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

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

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

   //原画像のヒストグラムを採る
   createHistogram(bmap_src,hist_R,hist_G,hist_B,hist_V);

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

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

   //----------------- ヒストグラムの補正(拡張) -------- ここから -------------------

   int PERCENT=5;
   int level_low,level_high;

   //ヒストグラムVのPERCENT*0.05を求めて表示する
   sum=0;
   i=0;
   do{
      sum+=hist_V[i];
      i++;
   }while(sum<SIZE*PERCENT*0.01);
   level_low=i-1;
   gr->DrawLine(Pens::Black,X1+level_low,Y0,X1+level_low,Y0+240);

   //ヒストグラムVの1-PERCENT*0.01を求めて表示する
   sum=0;
   i=255;
   do{
      sum+=hist_V[i];
      i--;
   }while(sum<SIZE*PERCENT*0.01);
   level_high=i+1;
   gr->DrawLine(Pens::Black,X1+level_high,Y0,X1+level_high,Y0+240);

   //ヒストグラムの補正(拡張)
   float ratio=255.0f/(level_high-level_low);
   for(j=0;j<HEIGHT;j++)
      for(i=0;i<WIDTH;i++){
         color_src=bmap_src->GetPixel(i,j);
         r=color_src.R;
         g=color_src.G;
         b=color_src.B;
         if(r<level_low) r1=0;
         else if(r>level_high) r1=255;
         else r1=(int)(ratio*(r-level_low));
         if(g<level_low) g1=0;
         else if(g>level_high) g1=255;
         else g1=(int)(ratio*(g-level_low));
         if(b<level_low) b1=0;
         else if(b>level_high) b1=255;
         else b1=(int)(ratio*(b-level_low));
         color_dst=Color::FromArgb(r1,g1,b1);
         bmap_dst->SetPixel(i,j,color1);
      }

   //----------------- ヒストグラムの補正(拡張) -------- ここまで -------------------

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

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

   //補正画像のヒストグラムを採る
   createHistogram(bmap_dst,hist_R,hist_G,hist_B,hist_V);

   //ヒストグラムを表示する
   for(i=1;i<255;i++){       //0と255は表示しない(スケールオーバーすることがあるので)
      gr->DrawLine(Pens::Red,X1+i,Y1+60,X1+i,Y1+60-hist_R[i]/scale);
      gr->DrawLine(Pens::Green,X1+i,Y1+120,X1+i,Y1+120-hist_G[i]/scale);
      gr->DrawLine(Pens::Blue,X1+i,Y1+180,X1+i,Y1+180-hist_B[i]/scale);
      gr->DrawLine(Pens::Gray,X1+i,Y1+240,X1+i,Y1+240-hist_V[i]/scale);
   }

}

private: Void createHistogram(Bitmap^ bmap,array<int>^ hist_r,array<int>^ hist_g,array<int>^ hist_b,array<int>^ hist_v){

   Color color1;
   int i,j;
   int r,g,b,v;

   for(i=0;i<256;i++){
      hist_r[i]=0;
      hist_g[i]=0;
      hist_b[i]=0;
      hist_v[i]=0;
   }

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

         hist_r[r]++;
         hist_g[g]++;
         hist_b[b]++;
         hist_v[v]++;
      }

}

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

   //----------------- ヒストグラムの補正(平坦化) -------- ここから -----------------

   int AVERAGE=SIZE/256+1;

   //平坦化のための変換テーブルの作成
   array<int>^ table_R=gcnew array<int>(256);
   array<int>^ table_G=gcnew array<int>(256);
   array<int>^ table_B=gcnew array<int>(256);



   sum=0;
   j=0;
   for(i=0;i<256;i++){   //table_Rの作成
      sum+=hist_R[i];
      j+=sum/AVERAGE;
      sum %=AVERAGE;
      table_R[i]=j;
   }
   sum=0;
   j=0;
   for(i=0;i<256;i++){   //table_Gの作成
      sum+=hist_G[i];
      j+=sum/AVERAGE;
      sum %=AVERAGE;
      table_G[i]=j;
   }
   sum=0;
   j=0;
   for(i=0;i<256;i++){   //table_Bの作成
      sum+=hist_B[i];
      j+=sum/AVERAGE;
      sum %=AVERAGE;
      table_B[i]=j;
   }

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

   //----------------- ヒストグラムの補正(平坦化) -------- ここまで -----------------


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


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


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


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


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