2006.8.8.
石立 喬

Visual C++ 2005 Express Edition の易しい使い方(8)

――― 一次元DCTプログラムを作ってみよう ―――

 フォーム上にグラフを描く例として、一次元DCTを紹介する。ラジオボタン、コンボボックス、ボタンなどのコントロールを付加して使いやすくした。すでに紹介した「易しい使い方」で説明した部分は省略されている場合があるので注意して欲しい。

概要
 画像圧縮などに広く用いられているDCT(離散コサイン変換)を理解するために、簡単な矩形波の原波形をDCTして時間領域から周波数領域に変換し、周波数領域で制限を加えてから、再び逆DCTで時間領域に戻して、原波形がどのように劣化するかを示した。

DCT
 電気技術者には、フーリエ変換(Fourier Transform)は馴染み深いものである。数値計算で求めると、それはDFT(Discrete Fourier Transform、離散フーリエ変換)となり、さらに有名な高速アルゴリズムを用いるとFFT(Fast Fourier Transform)になる。FFTは、角度に対する三角関数の値に符号の異なる同じものが繰返し発生することを利用し、それらを相殺して計算を簡略化するものである。フーリエ変換は、原波形が実数で表されるとすると、変換結果が実数と虚数のペアで求まる。
 DCT(Discrete Cosine Transform、離散コサイン変換)は、変換結果が実数のみになる特徴があり、このために画像圧縮などに広く用いられている。DCTにも高速化アルゴリズムが使えるが、ただ真似をするだけなのであまり興味がなく、一方、CPUの高速化で、パワーをもてあまし気味なので、DCTの式の通りにプログラムを作成した。
 フーリエ変換(正確にはフーリエ級数)の場合には、原波形が一定の周期で無限に繰返すものとの仮定がある。図1で、一番上は原波形で、その下は、それが繰返された状態を示す。一方、離散コサイン変換(DCT)では、原波形が鏡像関係に折り返しながら無限に続くとの仮定があり、それを一番下に示す。離散コサイン変換の結果が実数のみになる理由がそこにあるが、原波形の形状によっては歪みが生じる恐れがあり、これを緩和するために、窓関数を用いる方法もある。
 図2は、離散コサイン変換に適した形の原波形の例を示す。タイプAは従来型の原波形で、タイプBが離散コサイン変換向きの原波形である。しかし、この場合は、フーリエ変換で逆に不都合が生じる。結局、このプログラムでは、タイプAとタイプBの2種類の原波形を比較することにした。

図1 原波形に対するフーリエ変換の仮定と離散コサイン変換の仮定


図2 原波形にタイプBを用いると離散コサイン変換でも連続性が保てる


一次元DCTに使用する式
 DCTの式には数種類があり、それぞれ一長一短があるが、ここでは下記の分かり易いものを選んだ。


ただし、


フォームの設定

1)「Form1」のプロパティで、Size -- 600,580、BackColor -- Window、Text -- 「一次元DCTの実験」とする。
2)ツールボックスから「GroupBox」を持ってきて、フォーム左上に配置する(Text ---- 原波形)。
3)ツールボックスから「RadioButton」を2回持ってきて、グループボックスの中に配置する((Name) --- radioA、radioB、)。
4)「Label」を持ってくる(Text ---- 「uの上限値」)。
5)「ComboBox」を持ってくる(DropDownStyle ---- DropDownList)。コンボボックスの項目を設定するには、comboBox1のプロパティで、「データ」欄の「Item」をクリックし、右端の「三角矢印」をクリックする。「文字列コレクションエディタ」が開くので、1行ずつ項目名を入力し、「OK」をクリックする。
6)「Button」を持ってくる((Name) ---- buttonExe、Text ---- 「実行」)。

プログラムの構成
1) 主要なプログラムは、Form1_Paint()メソッドに記述する。このメソッドは、Formが画面上に現れる度に呼び出される。主な内容は次の通り。
  ・ 原波形f(x)を生成する。ただし、タイプAとタイプBを区別するために、xをシフトする(タイプBのみ)。
  ・ 原波形f(x)を赤色で一番上のグラフに描く。
  ・ 原波形f(x)をDCT処理して、DCTデータg(u)を得る。ただし、uの範囲は0 〜 u_max-1とする。結果を黄色で真ん中のグラフに描く。
  ・ DCTデータg(u)を逆DCT処理して、波形h(x)を得る。ただし、uの範囲は0 〜 u_max-1とする。結果を明るい緑色で一番下のグラフに描く。
2) フォームが最初に起動されたときに一度だけ実行されるForm1_Load()メソッドに、ラジオボタンの初期設定(原波形のタイプAを選択する)とコンボボックスの初期設定(uの最大限を512に設定する)を行なう。
3) コンボボックスが変更されると、comboBox1_SelectedIndexChanged()メソッドが呼び出されるので、選ばれたIndex番号から、u_maxを求めて設定する。
4) 「実行」ボタンがクリックされると、buttonExe_Click()メソッドが呼び出されるので、ラジオボタンの設定により値shiftを選択し、this->Refresh()でForm1_Paint()メソッドを呼び出す。

 配列の宣言は、従来の方法と少し異なる。一次元配列の場合は、
   array<型名>^  配列名=gcnew array<型名>(添字の個数);
とし、二次元では、
   array<型名,2>^  配列名=gcnew array<型名,2>(添字の個数,添字の個数);
とする。

定数M_PIや三角関数を使用するため、ヘッダファイルstdafx.hに、
   #define _USE_MATH_DEFINES
   #include <math.h>
を記述しておく。

プログラムの実際

   //クラスのグローバル変数
   private: static int N=512;
   int shift;

//フォームが表示、または再表示されたときのメソッド
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) {

   Graphics^ gr=e->Graphics;

   int X0=10,X1=525;
   int Y0=140,Y1=300,Y2=460;

   int N=512;
   int y,old_y;

   array<double>^ f=gcnew array<double>(N);    //原波形のための配列
   array<double>^ g=gcnew array<double>(N);    //DCT後のデータのための配列
   array<double>^ h=gcnew array<double>(N);    //InverseDCT後の波形のための配列

   System::Drawing::Font^ font1=gcnew System::Drawing::Font("MS ゴシック",10);

   //グラフの背景を描く
   for(int i=0;i<3;i++){
      //黒で背景を描く
      gr->FillRectangle(Brushes::Black,X0,Y0-75+160*i,512,150);
      //明るいグレイで横線を描く
      gr->DrawLine(Pens::LightGray,X0,Y0+160*i,X0+512,Y0+160*i);
   }

   //原波形(矩形波)を生成する。タイプAとタイプBはshiftで区別する
   for(int x=0;x<N;x++){
      if((x+shift) % (N/4)>N/8) f[x]=50.0;
      else f[x]=-50.0;
   }

   //原波形(矩形波)を描く
   for(int x=0;x<N;x++){
      y=Y0-(int)f[x];
      if(x==0) gr->DrawLine(Pens::Red,X0+x,y,X0+x,y);
      else gr->DrawLine(Pens::Red,X0+(x-1),old_y,X0+x,y);
      old_y=y;
   }
   gr->DrawString("原波形",font1,Brushes::Black,X1,Y0-5);

   //DCT結果を描く
   g[0]=0.0;
   for(int x=0;x<N;x++)
      g[0]+=f[x];
   g[0]=g[0]/sqrt((double)N);
   gr->DrawLine(Pens::Yellow,X0,Y1,X0,Y1-(int)(0.06*g[0]));
   for(int u=1;u<u_max;u++){
      g[u]=0.0;
      for(int x=0;x<N;x++)
         g[u]+=f[x]*cos(M_PI*(2*x+1)*u/(2*N));
      g[u]*=sqrt(2.0/N);
      gr->DrawLine(Pens::Yellow,X0+u,Y1,X0+u,Y1-(int)(0.06*g[u]));
   }
   gr->DrawString("DCTデータ",font1,Brushes::Black,X1,Y1-5);

   //InverseDCT結果を描く
   for(int x=0;x<N;x++){
      h[x]=0.0;
      for(int u=1;u<u_max;u++)
         h[x]+=g[u]*cos(M_PI*(2*x+1)*u/(2*N));
      h[x]=h[x]*sqrt(2.0/N)+g[0]/sqrt((double)N);
      y=Y2-(int)h[x];
      if(x==0) gr->DrawLine(Pens::LightGreen,X0+x,y,X0+x,y);
      else gr->DrawLine(Pens::LightGreen,X0+(x-1),old_y,X0+x,y);
      old_y=y;
   }
   gr->DrawString("IDCT波形",font1,Brushes::Black,X1,Y2-5);

}

//コンボボックスが変更されたときのメソッド
private: System::Void comboBox1_SelectedIndexChanged(System::Object^ sender, System::EventArgs^ e) {

   switch(comboBox1->SelectedIndex){
      case 0:u_max=512;break;
      case 1:u_max=256;break;
      case 2:u_max=128;break;
      case 3:u_max=64;break;
      case 4:u_max=32;break;
      case 5:u_max=16;break;
      default:u_max=512;break;
   }

}

//フォームを最初に表示したときのメソッド
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
   radioA->Checked=true; //ラジオボタン[タイプA]を選択する
   comboBox1->SelectedIndex=0; //uの上限値をに初期設定する
}

//[実行]ボタンをクリックしたときのメソッド
private: System::Void buttonExe_Click(System::Object^ sender, System::EventArgs^ e) {
   if(radioA->Checked==true) shift=0;
   if(radioB->Checked==true) shift=32;
   this->Refresh(); //グラフを再描画する
}


得られた画面
 図3は、プログラムを起動した直後の初期設定のままの画面を示す。原波形はタイプAが用いられており、IDCT(逆DCT)後の再現波形で折り返しの欠点が出やすい。「uの上限値」が最高の512に設定されているので、IDCT波形は原波形と等しく(若干の計算誤差があるが…)、折り返しの効果は出ていない。


図3 原波形をタイプAとし、uの上限を512とした場合のフォーム


 図4は、原波形をタイプBに変更し、折り返し効果をなくしてある。「uの上限値」を32に設定してあるので、IDCT波形がかなり歪んでいる(高調波成分が失われている)。

図4 原波形をタイプBとし、uの上限値を32とした場合のフォーム


 図5は、uの上限値を32にした場合のタイプA波形とタイプB波形とを比較したものである。これによれば、IDCT結果において、タイプAでは両端に波形歪が生じているのが分かる。

図5 原波形がタイプAかタイプBかで、IDCT結果に差がでる