2.3 離散 Cosine/Sine 変換

2.3.3 DCT/DST 専用の FFT ルーチンを用いる方法

タイプ II, III に対する第一の方法

 実数データの DCT を計算するもっとも単純な方法は,データの長さを 倍にしてデータを対称にしてから,実数データに対する拡張 FFT で計算する ことです.しかし,これは半分の無駄な計算が含まれます.ここでは, データの対称性を用いて,演算量と記憶領域を実数データの拡張 FFT の 1/2 (複素数データの拡張 FFT の1/4) にする方法について示します [参考文献].

 最初に,DCT-II :

         N-1                      
    A  = Σ   a   cos(π(j+1/2)k/N) , 0 <= k < N   
     k   j=0  j                   

は,対称なデータの拡張 DFT :

         2N-1                        
    A  =  Σ   b   exp(2π(j+1/2)k/2N) , 0 <= k < N   
     k    j=0  j                     

に相当します.ここで,配列 b は a を対称にしたもので,

                    1     
    b  = b       = --- a  , 0 <= j < N   
     j    2N-1-j    2   j 

です.この対称性は,実数データに対する拡張 FFT の log(N) 回ある すべての段のデータに複素共役対称として波及していきます.そして, 最後の段で A_k が A_k 自身の複素共役対称となり,実数に戻ります. この冗長な演算と記憶領域は, 実数データの拡張 FFTの リスト 2.2-2 の拡張 FFT のバタフライループの長さを半分にすることで簡単に 取り除くことができます.

 次に,対称性を用いて実数に対する拡張 FFT の演算量と記憶領域を 半分にした DCT-II のルーチンを示します.

リスト2.3.3-1. 拡張 FFT の冗長な演算を取り除いた FCT-II
/*
  離散コサイン変換 (Unnormalized DCT-II) :
  F[k]=Σ_j=0^n-1 a[j]*cos(pi*(j+1/2)*k/n),0<=k<n を計算する.
*/
#include <math.h>

void fct_ii(int n, double a[])
{
    int m, mh, mq, i, j, k, irev, jr, ji, kr, ki;
    double theta, wr, wi, xr, xi;

    /* ---- scrambler ---- */
    i = 0;
    for (j = 1; j < n - 1; j++) {
        for (k = n >> 1; k > (i ^= k); k >>= 1);
        if (j < i) {
            xr = a[j];
            a[j] = a[i];
            a[i] = xr;
        }
    }
    theta = -4 * atan(1.0) / n;
    for (mh = 2; (m = mh << 1) <= n; mh = m) {
        mq = mh >> 1;
        /* ---- real to real & complex butterflies ---- */
        irev = 0;
        for (jr = 0; jr < n; jr += m) {
            wr = cos(0.5 * theta * (irev + mq));
            wi = sin(0.5 * theta * (irev + mq));
            for (k = n >> 2; k > (irev ^= k); k >>= 1);
            ki = jr + mq;
            kr = n - ki;
            ji = kr - mq;
            xr = a[jr] - a[kr];
            xi = -a[ji] + a[ki];
            a[jr] += a[kr];
            a[ji] += a[ki];
            a[ki] = wr * xr + wi * xi;
            a[kr] = wr * xi - wi * xr;
        }
        if (mh == 2) continue;
        /* ---- complex to complex butterflies ---- */
        irev = 0;
        for (i = 0; i < n; i += m) {
            wr = cos(theta * (irev + mq));
            wi = sin(theta * (irev + mq));
            for (k = n >> 2; k > (irev ^= k); k >>= 1);
            for (j = 1; j < mq; j++) {
                jr = i + j;
                ki = i + mh - j;
                ji = n - jr;
                kr = n - ki;
                xr = a[jr] - a[kr];
                xi = -a[ji] - a[ki];
                a[jr] += a[kr];
                a[ji] -= a[ki];
                a[ki] = wr * xr + wi * xi;
                a[kr] = wr * xi - wi * xr;
            }
        }
    }
    if (n > 1) {
        m = n >> 1;
        xr = a[0] - a[m];
        a[0] += a[m];
        a[m] = xr * cos(0.5 * theta * m);
    }
}
 

このルーチンの N=8 のときのデータフローは次のようになります.

図2.3.3-1. 拡張 FFT の冗長な演算を取り除いた FCT-II

 次に,この手続きを逆にした DCT-III に対するルーチンを示します.

リスト2.3.3-2. 拡張 FFT の冗長な演算を取り除いた FCT-III
/*
  逆離散コサイン変換 (Unnormalized DCT-III) :
  a[k]=Σ_j=0^n-1 F[j]*cos(pi*j*(k+1/2)/n),0<=k<n を計算する.
*/
#include <math.h>

void fct_iii(int n, double a[])
{
    int m, mh, mq, i, j, k, irev, jr, ji, kr, ki;
    double theta, wr, wi, xr, xi;

    theta = 4 * atan(1.0) / n;
    if (n > 1) {
        m = n >> 1;
        xr = a[m] * cos(0.5 * theta * m);
        a[m] = a[0] - xr;
        a[0] += xr;
    }
    for (m = n; (mh = m >> 1) >= 2; m = mh) {
        mq = mh >> 1;
        /* ---- real & complex to real butterflies ---- */
        irev = 0;
        for (jr = 0; jr < n; jr += m) {
            wr = cos(0.5 * theta * (irev + mq));
            wi = sin(0.5 * theta * (irev + mq));
            for (k = n >> 2; k > (irev ^= k); k >>= 1);
            ki = jr + mq;
            kr = n - ki;
            ji = kr - mq;
            xr = wr * a[ki] + wi * a[kr];
            xi = wr * a[kr] - wi * a[ki];
            a[kr] = a[jr] - xr;
            a[ki] = a[ji] + xi;
            a[jr] += xr;
            a[ji] -= xi;
        }
        if (mh == 2) continue;
        /* ---- complex to complex butterflies ---- */
        irev = 0;
        for (i = 0; i < n; i += m) {
            wr = cos(theta * (irev + mq));
            wi = sin(theta * (irev + mq));
            for (k = n >> 2; k > (irev ^= k); k >>= 1);
            for (j = 1; j < mq; j++) {
                jr = i + j;
                ki = i + mh - j;
                ji = n - jr;
                kr = n - ki;
                xr = wr * a[ki] + wi * a[kr];
                xi = wr * a[kr] - wi * a[ki];
                a[kr] = a[jr] - xr;
                a[ki] = -a[ji] - xi;
                a[jr] += xr;
                a[ji] -= xi;
            }
        }
    }
    /* ---- unscrambler ---- */
    i = 0;
    for (j = 1; j < n - 1; j++) {
        for (k = n >> 1; k > (i ^= k); k >>= 1);
        if (j < i) {
            xr = a[j];
            a[j] = a[i];
            a[i] = xr;
        }
    }
}
 

 この方法は,基数を大きくしたり Split-Radix FFT にしたりすることで, 演算量はさらに減らすことができます.さらに, 混合基数にすることで任意の 長さの DCT が計算できます.

タイプ II, III に対する第二の方法

 ここでは,Cooley-Tukey 型 FFT と同様の考え方で,長さ N の DCT-II を 二つの N/2 の長さの DCT-II に分解することで導かれる高速コサイン変換を 示します.この考えに基づく高速コサイン変換は B.G. Lee [参考文献] による 高速コサイン変換として知られているようです.

 まず,DCT-II :

         N-1                      
    A  = Σ   a   cos(π(j+1/2)k/N) 
     k   j=0  j                   

         N-1      (j+1/2)k 
       = Σ   a   C         , 0 <= k < N   
         j=0  j   N        

を次のように分解します.

          N/2-1               (j+1/2)k  
    A   =  Σ   (a + a      ) C          
     2k    j=0   j   N-1-j    N/2       


                  N/2-1                j+1/2  (j+1/2)k  
    A    + A    =  Σ   (a − a      )2 C      C          
     2k-1   2k+1   j=0   j   N-1-j     N      N/2       

ここで,C は

     j              
    C  = cos(πj/N)  
     N              

です.この分解は,基数 2 の周波数間引き FFT の分解と似ていますが, A の奇数の項の分解が異なり,簡単な漸化式になっています.したがって, 通常のバタフライ演算以外に後処理が必要になります.具体的には, まず分解の式に K=0 を代入し,1/2 倍して初項 A_1 を求めます (A_-1 = A_1 という関係から).次に,逐次的に A の奇数項を求めて行きます. この後処理はスケーリングを除外すれば加減算だけで計算できます.しかし, これはバタフライの段数-1 回行わなければなりません.

 次に,この方法による DCT-II のルーチンを示します.

リスト2.3.3-3. B.G. Lee による FCT-II
/*
  離散コサイン変換 (Unnormalized DCT-II) :
  F[k]=Σ_j=0^n-1 a[j]*cos(pi*(j+1/2)*k/n),0<=k<n を計算する.
*/
#include <math.h>

void fct_ii(int n, double a[])
{
    int m, mh, i, j, k, irev, j0, j1;
    double theta, c2, xr;

    /* ---- scrambler ---- */
    i = 0;
    for (j = 1; j < n - 1; j++) {
        for (k = n >> 1; k > (i ^= k); k >>= 1);
        if (j < i) {
            xr = a[j];
            a[j] = a[i];
            a[i] = xr;
        }
    }
    theta = 2 * atan(1.0) / n;
    /* ---- butterflies ---- */
    for (mh = 1; (m = mh << 1) <= n; mh = m) {
        irev = 0;
        for (i = 0; i < n; i += m) {
            c2 = 2 * cos(theta * (irev + mh));
            for (k = n >> 1; k > (irev ^= k); k >>= 1);
            for (j = 0; j < mh; j++) {
                j0 = i + j;
                j1 = n - mh - i + j;
                xr = a[j0] - a[j1];
                a[j0] += a[j1];
                a[j1] = c2 * xr;
            }
        }
    }
    /* ---- adds ---- */
    for (m = n >> 1; (mh = m >> 1) >= 1; m = mh) {
        for (j = 0; j < mh; j++) {
            j0 = m + mh + j;
            a[j0] = -a[j0] - a[j0 - m];
        }
        for (i = (m << 1) + mh; i < n; i += (m << 1)) {
            for (j = 0; j < mh; j++) {
                j0 = i + j;
                j1 = j0 + m;
                a[j0] -= a[j0 - m];
                a[j1] = -a[j1] - a[j0];
            }
        }
    }
    /* ---- scaling ---- */
    for (j = 1; j < n; j++) {
        a[j] *= 0.5;
    }
}
 

このルーチンの N=8 のときのデータフローは次のようになります.

図2.3.3-2. B.G. Lee による FCT-II

 次に,演算量について少し考えます.1/2 によるスケーリングは, バタフライ演算の回転因子に含めることができます.したがって, 長さ N の DCT-II の計算に必要な乗算回数 μ と加算回数 α は次のように なります.

         N         
    μ = --- log N  
         2         


         3N                 
    α = ---- log N - N + 1  
         2                  

この演算量は非常に少なく,拡張 FFT の計算量を半分にする第一の方法に 対して,Split-Radix FFT を用い,さらに複素乗算を 3 回の実数乗算と 3 回の 実数加算で計算したときの演算量に匹敵します.また,この B.G. Lee の方法に 関しても大きい基数や Split-Radix にすれば演算量は減らせると思うかも しれませんが,実際はそう甘くはなく演算量はまったく同じになります. 基数をいじることは,ループ展開による高速化技法としてしか意味を持たなく なります.しかし,二次元 DCT を計算する場合は通常の FFT 同様に,後に示す Vector-Radix FFT を用いれば 乗算回数だけをさらに 3/4 に減らすことが可能になります.

 DCT-III に関しても,この手続きを逆にすることで得られます. このとき,前処理は単なる和で直接計算できます.しかし,バタフライの 回転因子には 1/(2*cos(π*j/N)) と除算が入ってしまい,除算を 避けるようにあらかじめ計算しておかなければなりません.

 最後に,この方法の欠点を挙げておきます.それは,バタフライ演算と 後処理の漸化式の計算で,長さ N に比例する丸め誤差が入るということです. したがって,あまり大きな計算には向きません.

タイプ II, III に対する第三の方法

 ここでは,長さ N の DCT-II を一つの N/2 の長さの DCT-II と二つの N/4 の長さの DCT-II に分解することによる高速コサイン変換を示します.

 まず,DCT-II :

         N-1                      
    A  = Σ   a   cos(π(j+1/2)k/N) , 0 <= k < N   
     k   j=0  j                   

を次のように N/2 の長さの DCT-II と DCT-IV に分解します.

          N/2-1                                    
    A   =  Σ   (a + a      ) cos(π(j+1/2)k/(N/2))  
     2k    j=0   j   N-1-j                         


           N/2-1                                         
    A    =  Σ   (a − a      )cos(π(j+1/2)(k+1/2)/(N/2))  
     2k+1   j=0   j   N-1-j                              

次に,N/2 の長さの DCT-IV を 2.3.2 で示した DCT-IV に対する分解を 用いて二つの N/4 の長さの DCT-II にします.この分解を繰り返すことで 高速算法が得られます.このとき同時に,DCT-IV に対する高速算法も 得られます.この高速算法は Split-Radix FFT とよく似た構造で L 字型の バタフライとなり,log(N) 段の演算は一段ずつ終了しなくなります. さらに,加減算からなる後処理が必要で多少複雑になります.また, この方法の演算量は DCT-IV の分解に現れる複素乗算を 3 回の加算と 3 回の乗算に置き換えたとき,B.G. Lee の方法とほぼ同じになります.

 この方法の分解は,N 点の DCT を通常の DFT だと思って, Cooley-Tukey 型 FFT の分解を素直に用いたものです.この分解を表にすると 次のようになります.

リスト2.3.3-4. DCT の分解
周波数間引きに相当する DCT の分解
N+1 点 DCT-I N+1 回の加算により N/2+1 点 DCT-I と N/2 点 DCT-III に分解
N 点 DCT-II N 回の加算により N/2 点 DCT-II と N/2 点 DCT-IV に分解
N 点 DCT-IV N-2 回の加算と N/2 回の複素数乗算により二組の N/2 点 DCT-II に分解
時間間引きに相当する DCT の分解
N+1 点 DCT-I N+1 回の加算により N/2+1 点 DCT-I と N/2 点 DCT-II に分解
N 点 DCT-III N 回の加算により N/2 点 DCT-III と N/2 点 DCT-IV に分解
N 点 DCT-IV N-2 回の加算と N/2 回の複素数乗算により二組の N/2 点 DCT-III に分解

この表から,長さ N の DCT を単純に半分の長さに分解したとき,別の型の 変換が混ざり,通常のDFT のように自分自身の変換には戻ってくれません. しかし,自分自身の変換に戻らなくても,この分解は繰り返すことができて, 四つのタイプのすべてが高速に計算できます.


目次へ戻る