2.4 離散 Hartley 変換

2.4.2 離散 Hartley 変換に対する FFT ルーチン

 離散 Hartley 変換は,通常の DFT と同様に Cooley-Tukey 型の FFT の 考えが適用できます.離散 Hartley 変換 :

          N-1                                                 
    A  =  Σ   a   cas(2πjk/N)   ,   cas(x) = cos(x) + sin(x)  
     k    j=0  j                                              

に対する周波数間引きの基数 2 の分解は次のようになります.

          N/2-1                      
    A   =  Σ   r   cas(2πjk/(N/2))   
     2k    j=0  j                    


            N/2-1                      
    A     =  Σ   s   cas(2πjk/(N/2))   
     2k+1    j=0  j                    


                     
    r  = a + a       
     j    j   N/2+j  


                                                           
    s  = (a − a     )cos(2πj/N) + (a     − a   )sin(2πj/N) 
     j     j   N/2+j                N/2-j   N-j            

通常の DFT の分解と比較すると,奇数の項の分解が少し複雑になり, a_N/2-j と a_N-j という余計な項が入ってきます.したがって,データの 上書きをする場合は,r_j と r_N/2-j と s_j と s_N/2-j の四つの項を 同時に演算する必要があります.このとき,s_j と s_N/2-j の計算で 複素数乗算に相当する演算が含まれ,通常の複素 FFT のバタフライ演算と 同じ構造になってしまいます.この変更を行った高速 Hartley 変換 (FHT) の ルーチンは次のようになります.

リスト2.4.2-1. 周波数間引き基数 2 の FHT
/*
  離散 Hartley 変換 :
  F[k]=Σ_j=0^n-1 a[j]*(cos(2*pi*j*k/n)+sin(2*pi*j*k/n)),0<=k<n
  を計算する. n はデータ数で 2 の整数乗.
*/
#include <math.h>

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

    theta = 8 * atan(1.0) / n;
    for (m = n; (mh = m >> 1) >= 1; m = mh) {
        mq = mh >> 1;
        for (i = 0; i <= mq; i++) {
            /* ---- (cos + sin) == 1 ---- */
            if (i == 0 || i == mq) {
                for (jr = i; jr < n; jr += m) {
                    kr = jr + mh;
                    xr = a[jr] - a[kr];
                    a[jr] += a[kr];
                    a[kr] = xr;
                }
                continue;
            }
            /* ---- (cos + sin) != 1 ---- */
            wr = cos(theta * i);
            wi = sin(theta * i);
            for (j = 0; j < n; j += m) {
                jr = i + j;
                ji = mh - i + j;
                kr = jr + mh;
                ki = ji + mh;
                xr = a[jr] - a[kr];
                xi = a[ji] - a[ki];
                a[jr] += a[kr];
                a[ji] += a[ki];
                a[kr] = wr * xr + wi * xi;
                a[ki] = wi * xr - wr * xi;
            }
        }
        theta *= 2;
    }
    /* ---- 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;
        }
    }
}
 

このルーチンは 2 バタフライルーチンで,cas(x) = 1 のときの例外処理を 行っています.このルーチンのフローグラフは次のようになります.

図2.4.2-1. 周波数間引き基数 2 の FHT

 この FHT のルーチンから通常の DFT が簡単に計算できます.FHT から 通常の実数の DFT に変換するルーチンを示しておきます.

リスト2.4.2-2. FHT を間接的に用いる実数 FFT
/*
  実数データの DFT :
  F[k]=Σ_j=0^n-1 a[j]*exp(-2*pi*i*j*k/n),0<=k<n を計算する.
  出力 a[0...n/2], a[n/2+1...n-1] はそれぞれ F[0...n/2] の実部, 
  F[n/2+1...n-1] の虚部に相当する.
*/

void rfft(int n, double a[])
{
    void fht(int n, double a[]);
    int nh, j;
    double x;

    fht(n, a);
    nh = n / 2;
    for (j = 1; j < nh; j++) {
        x = 0.5 * (a[j] - a[n - j]);
        a[j] -= x;
        a[n - j] = x;
    }
}


/*
  実数データの DFT の逆変換:
  F[k] = (a[0]+a[n/2]*(-1)^k)/2 + Σ_j=1^n/2-1 a[j]*cos(2*pi*j*k/n)
                                + Σ_j=1^n/2-1 a[n-j]*sin(2*pi*j*k/n)
  を計算する. 
  注意: スケーリングは 2/n しなければいけない.
*/

void irfft(int n, double a[])
{
    void fht(int n, double a[]);
    int nh, j;
    double x;

    nh = n / 2;
    a[0] *= 0.5;
    a[nh] *= 0.5;
    for (j = 1; j < nh; j++) {
        x = 0.5 * (a[j] - a[n - j]);
        a[j] -= x;
        a[n - j] = x;
    }
    fht(n, a);
}
 

また,複素 DFT も同様に FHT から計算できます.

 次に,FHT の基数について考えます.通常の FFT と同様に,FHT も, 混合基数や大きい基数, さらに Split-Radix にすることができて,通常のFFT と同じ割合で演算量を削減できます. 次に,Split-Radix による FHT のルーチンを示します.

リスト2.4.2-3. Split-Radix FHT
/*
  離散 Hartley 変換 :
  F[k]=Σ_j=0^n-1 a[j]*(cos(2*pi*j*k/n)+sin(2*pi*j*k/n)),0<=k<n
  を計算する. n はデータ数で 2 の整数乗.
*/
#include <math.h>

void fht(int n, double a[])
{
    int m, mq, mqh, i, j, k;
    int j0r, j0i, j1r, j1i, j2r, j2i, j3r, j3i;
    double theta, w1r, w1i, w3r, w3i, x0r, x0i, x1r, x1i, x3r, x3i;

    /* ---- L shaped butterflies ---- */
    theta = 8 * atan(1.0) / n;
    for (m = n; m > 4; m >>= 1) {
        mq = m >> 2;
        mqh = mq >> 1;
        /* ---- (cos + sin) == sqrt(2) ---- */
        w1r = 2 * cos(theta * mqh);
        for (k = m; k <= n; k <<= 2) {
            for (j0r = k - m; j0r < n; j0r += 2 * k) {
                j1r = j0r + mqh;
                j0i = j0r + mq;
                j1i = j1r + mq;
                j2r = j0i + mq;
                j3r = j1i + mq;
                j2i = j2r + mq;
                j3i = j3r + mq;
                x1r = a[j0r] - a[j2r];
                x1i = a[j0i] - a[j2i];
                a[j0r] += a[j2r];
                a[j0i] += a[j2i];
                a[j2r] = x1r + x1i;
                a[j2i] = x1r - x1i;
                x3r = w1r * (a[j1r] - a[j3r]);
                x3i = w1r * (a[j1i] - a[j3i]);
                a[j1r] += a[j3r];
                a[j1i] += a[j3i];
                a[j3r] = x3r;
                a[j3i] = x3i;
            }
        }
        for (i = 1; i < mqh; i++) {
            /* ---- (cos + sin) != sqrt(2) ---- */
            w1r = cos(theta * i);
            w1i = sin(theta * i);
            w3r = cos(theta * 3 * i);
            w3i = sin(theta * 3 * i);
            for (k = m; k <= n; k <<= 2) {
                for (j = k - m; j < n; j += 2 * k) {
                    j0r = i + j;
                    j1r = mq - i + j;
                    j0i = j1r + mq;
                    j1i = j0r + mq;
                    j2r = j1i + mq;
                    j3r = j0i + mq;
                    j2i = j3r + mq;
                    j3i = j2r + mq;
                    x1r = a[j0r] - a[j2r];
                    x1i = a[j0i] - a[j2i];
                    a[j0r] += a[j2r];
                    a[j0i] += a[j2i];
                    x3r = a[j1r] - a[j3r];
                    x3i = a[j1i] - a[j3i];
                    a[j1r] += a[j3r];
                    a[j1i] += a[j3i];
                    x0r = x1r + x3r;
                    x0i = x1i - x3i;
                    a[j2r] = w1r * x0r + w1i * x0i;
                    a[j3r] = w1i * x0r - w1r * x0i;
                    x0r = x1r - x3r;
                    x0i = x1i + x3i;
                    a[j3i] = w3r * x0r + w3i * x0i;
                    a[j2i] = w3i * x0r - w3r * x0i;
                }
            }
        }
        theta *= 2;
    }
    /* ---- radix 2 butterflies (m == 4) ---- */
    for (k = 4; k <= n; k <<= 2) {
        for (j = k - 4; j < n; j += 2 * k) {
            x0r = a[j] - a[j + 2];
            x0i = a[j + 1] - a[j + 3];
            a[j] += a[j + 2];
            a[j + 1] += a[j + 3];
            a[j + 2] = x0r;
            a[j + 3] = x0i;
        }
    }
    /* ---- radix 2 butterflies (last stage) ---- */
    if (n > 1) {
        for (j = 0; j < n; j += 2) {
            x0r = a[j] - a[j + 1];
            a[j] += a[j + 1];
            a[j + 1] = x0r;
        }
    }
    /* ---- unscrambler ---- */
    i = 0;
    for (j = 1; j < n - 1; j++) {
        for (k = n >> 1; k > (i ^= k); k >>= 1);
        if (j < i) {
            x0r = a[j];
            a[j] = a[i];
            a[i] = x0r;
        }
    }
}
 

このルーチンは,log(N) 段ある演算で,Split-Radix が意味を持たなくなった 時点で,基数 2 に切り替えています.したがって,通常の FFT に比べて, 切り替えを一段だけ早くしてあります.また,このルーチンは 2 バタフライの Split-Radix FFT で凝ったことは何もしていないのですが,不要な乗算は すべて取り除かれています.

 次に,FHT の演算量と通常の FFT の演算量とを比較してみます.上の Split-Radix FHT のルーチンの乗算回数 μ と加算回数 α は次のように なります.

         2             19           1       log N  
    μ = --- N log N - ---- N + 3 + --- (-1)        
         3             9            9              


         4             8           1       log N  
    α = --- N log N - --- N + 1 - --- (-1)        
         3             9           9              

また,複素数乗算に相当する演算を 3 回の実数乗算と 3 回の実数加算で 計算すれば

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


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

となります.これと通常の複素 Split-Radix FFT の演算量とを比較すると 演算量はちょうど半分になります.しかし,複素 DFT が二回の FHT で 計算できることを考慮すると,演算量はまったく同じということになります. さらに,FHT を DFT に変換するときの加減算も考慮すると,かえって不利に なります.しかし,FHT は複素 FFT に比べて少ないバタフライ数で済むため, 実際の計算では高速になるかもしれません.FHT は複素 FFT に比べて高速だと よく言われるようですが,FHT と FFT の比較を実際にインプリメントする ことで調べた文献[参考文献] によるとその差はほとんどないようです.


目次へ戻る