2010. 5.28.
石立 喬

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

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

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

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

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

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



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


一次元DCTに使用する式
 DCTの式には数種類があり、それぞれ一長一短があるが、ここでは下記の分かり易いものを選んだ。xが時間軸上の値に相当し、uは各周波数軸上の値に相当する。

     
ただし、

     

フォームへの部品の配置

1)「Form1」の「プロパティ」ウインドウの「配置」欄で、「Size」を「600,585」、「表示」欄で「BackColor」を「Window」、「Font」を「MS ゴシック、9.75pt」、「Text」を 「一次元DCTの実験」とする。
2)「ツールボックス」ウインドウの「コンテナ」欄の「GroupBox」でクリックして、次にフォーム上の左上で再度クリックして、「GroupBox」を持ってくる。暫定的に置き、後で細かく場所やサイズを決める。
3)「ツールボックス」の「コンテナ」欄から「RadioButton」を2回持ってきて、グループボックスの中に配置する。
4)「ツールボックス」の「コモン コントロール」欄から「Label」を持ってくる。
5)同じく、「ツールボックス」の「コモン コントロール」欄から「ComboBox」を持ってくる。
2)から5)までの操作で、フォーム上に配置した部品を図3に示す。この段階では、まだ、最終的な位置やキャプションを決めていない。


図3 とりあえず、部品を配置したところ(名前はまだ付けてない)


各部品のプロパティの設定

1)「groupBox1」を右クリックして表示させた「プロパティ」ウインドウの「表示」欄で、「Text」を「原波形」とする。
2)「radioButton1」の「プロパティ」ウインドウでは、「デザイン」欄の(Name) を「radioA」にして、「表示」欄の「Text」は「タイプA」にする。「radioButton2」の「プロパティ」ウインドウでは、「デザイン」欄の(Name) を「radioB」にして、「表示」欄の「Text」は「タイプB」にする。
3)「label1」の「プロパティ」ウインドウでは、「デザイン」欄の(Name) はそのままにして、「表示」欄の「Text」は「uの上限値」にする。
4)「comboBox1」のプロパティで、「表示」欄の「DropDownStyle」を「DropDownList」にし、「データ」欄の「Items」をクリックし、右端の四角をクリックする。「文字列コレクションエディタ」が開くので、1行ずつ項目名を入力し、「OK」をクリックする。図4は、「文字列コレクション エディター」に選択肢としての数値を入力した状態を示す。


図4 「文字列コレクション エディタ」に入力し終わったところ 


プログラムの構成
 定数πや三角関数はMathクラスを使用する。名前空間はSystemであるので、特に設定する必要はない。
 gr->DrawLine()を使用してグラフを描画する方法については、すでに述べ通りで、最初はold_yに座標を設定するだけで、次回からold_yからyまで線を引く。
 一次元配列の宣言は、下記のようにして行う。
    array<型名>^  配列名=gcnew array<型名>(添字の個数);
 プログラムは、無駄をなくして高速化するよりも、可読性に重点を置いた。たとえば、Math::Sqrt(2.0/N)やMath::Sqrt((double)N)の計算が度々出てくるのは、本当は見苦しい。

1) フォームが最初に起動されたときに一度だけ実行されるForm1_Load()メソッドに、ラジオボタンの初期設定(原波形のタイプAを選択する)とコンボボックスの初期設定(uの最大限を512に設定する)を行なう。
2) 主要なプログラムは、Form1_Paint()メソッドに記述する。このメソッドは、Formが画面上に現れる都度、またはInvalidate()により呼び出されると実行する。主な内容は次の通り。
  ・ラジオボタンの状態を読み取って、shiftを0または32に設定する。
  ・コンボボックスの状態から、u_maxを設定する。
  ・グラフの背景を黒で描く。
  ・原波形f(x)を生成する。ただし、タイプAとタイプBを区別するために、xにshiftを加算する。
  ・原波形f(x)を赤色で一番上のグラフに描く。
  ・原波形f(x)をDCT処理して、DCTデータg(u)を得る。ただし、uの範囲は0 〜 u_max-1とする。結果を黄色で真ん中のグラフに描く。
  ・DCTデータg(u)を逆DCT処理して、波形h(x)を得る。ただし、uの範囲は0 〜 u_max-1とする。結果を明るい緑色で一番下のグラフに描く。
3) コンボボックスが変更されると、comboBox1_SelectedIndexChanged()メソッドが呼び出されるので、Paint()を呼び出す。
4) ラジオボタンのチェックが変わると、radioA_CheckedChanged()メソッドが呼び出されるので、Paint()を呼び出す。

プログラム

    //フォームを最初に表示したときのメソッド
    private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {

        radioA->Checked=true;     //最初は「タイプA」が選択されているようにする
        comboBox1->SelectedIndex=0;  //最初は「uの上限値」を「u512」にする

    }

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

        Graphics^ gr=e->Graphics;

        int X0=10,X1=525;
        int Y0=147,Y1=307,Y2=467;
        int N=512;
        int shift=(radioA->Checked==true)? 0:32;
        int u_max=int::Parse(comboBox1->Text);

        int i,u,x,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後の波形のための配列

        //グラフの背景を描く
        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) old_y=y;
            else{
                gr->DrawLine(Pens::Red,X0+(x-1),old_y,X0+x,y);
                old_y=y;
            }
        }
        gr->DrawString("原波形",Font,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]/Math::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]*Math::Cos(Math::PI*(2*x+1)*u/(2*N));
            g[u]*=Math::Sqrt(2.0/N);
            gr->DrawLine(Pens::Yellow,X0+u,Y1,X0+u,Y1-(int)(0.06*g[u]));
        }
        gr->DrawString("DCTデータ",Font,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]*Math::Cos(Math::PI*(2*x+1)*u/(2*N));
            h[x]=h[x]*Math::Sqrt(2.0/N)+g[0]/Math::Sqrt((double)N);
            y=Y2-(int)h[x];
            if(x==0) old_y=y;
            else{
                gr->DrawLine(Pens::LightGreen,X0+(x-1),old_y,X0+x,y);
                old_y=y:
            }
        }
        gr->DrawString("IDCT波形",Font,Brushes::Black,X1,Y2-5);

    }

    //「comboBox1」が変わったら再描画する
    private: System::Void comboBox1_SelectedIndexChanged(System::Object^ sender, System::EventArgs^ e) {

        Invalidate();

    }

    //「radioA」ボタンのチェックが変わったら再描画する
    private: System::Void radioA_CheckedChanged(System::Object^ sender, System::EventArgs^ e) {

        Invalidate();

    }


得られた画面
 図5は、プログラムを起動した直後の初期設定のままの画面を示す。原波形はタイプAが用いられており、IDCT(逆DCT)後の再現波形で折り返しの欠点が出やすい。「uの上限値」が最高の512に設定されているので、IDCT波形は原波形と等しく(若干の計算誤差があるが…)、折り返しの効果は出ていない。 「DCTデータ」波形を見ると、基本周波数よりもさらに低い所に出力が見られる。これは、「タイプA」をDCTしたからである。「タイプB」を用いた、後述の図6では、基本周波数より低い成分は見られない。



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


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


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


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

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



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