Excelにはフーリエ変換をする拡張機能がついていますが、とても使いにくいです。シートにデータがあって、取りあえずFFTをかけてみたい、どんな周期を持っているのか知りたい、という時にパッとは使えません。

 

そこで、取りあえずFFTをかけてグラフを作る、というマクロを作りました。周波数解析(周期解析)をしたい時に便利です。

 

機能やマクロの内容は次の通りです。

 

1.Excelのシートにある縦のデータ(列データ)をFFTに必要な個数で取得する。

2.そのデータをFFTにかけます。

3.窓関数(Hann窓、Hamming窓)の処理ができます。

4.FFT後の周期波形、位相波形をグラフにします。

5.周期波形の強度が強いものを100%ととして指定のレベル以上(カットオフレベル)の周期(周波数)だけを用いて逆FFTをかけて時間波形に戻します。

6.元の波形とレベル選別した波形をグラフにします。

7.この二つの波形の相関係数を計算します。

8.グラフはシートにデータを書かずに作成します。

9.ポイント数は2の乗数に制限されます。

2,4,8,16,32,64,128,256,512,1024,2048,4096,.....です。

 

 

使い方は、

1.FFTをかけたい列データのどこかのセル1つを選択します。

2.マクロ「FFTグラフ作成」を起動します。

3.「データの開始行」、「データ数」、「カットオフレベル」、「窓関数」を聞いてきますので順に指定してください。

4.三個のグラフが作成されます。

 

 

取得するデータの部分を変更していただければ都合のいいように使いまわせると思います。VBAはいつものことながら、あまりきれいなコードではありません。すいません。

結果の数値データがほしい時は、グラフにしているデータをシートに出すようにしていただければよいと思います。

グラフはExcelのチャートですので、ポイント数が多いと重くなります。ポイント数が多い時(512ポイントぐらい以上)に、作成後のグラフを修正しようとするとエラーになることもあります。場合によっては初めからエラーになったりするかもしれません。

 

 

こころの相談室おうみ/小倉

 


Sub FFTグラフ作成()
    Dim AC As Range, CLS As Range, CLE As Range
    Dim iyS As Long, iyE As Long, ix As Long
    Dim inFFT As Long, iwind As Long
    Dim fftdataR() As Double, fftdataI() As Double, fftdataAbs() As Double
    Dim fftPeriod() As Double, fftFreq() As Double, theta() As Double
    Dim fftdataOrg() As Double, DataNum() As Long
    Dim PeakLevel As Double, CorrelationCoef As Double, CutOffPercent As Double
    Dim strtemp As String, i As Long
    Dim fftdataABSperinFFT() As Double, CutOffLevel() As Double
    Dim objChart As Object, chartTtl As String, ItemName As String

    Const pai = 3.14159265

    Application.ScreenUpdating = False
    
    'データの範囲と項目名
    Set AC = ActiveCell
    Set CLS = AC.End(xlUp) 'データの一番上
    Set CLE = AC.End(xlDown) 'データの一番下
    If IsNumeric(CLS.Value) = False Then 'データの一番上が数値でない時
        ItemName = CLS.Value 'そこを項目名とみなす
        iyS = CLS.Offset(1, 0).Row 'その1行下からデータとみなす
    Else                            'データの一番上が数値の時
        ItemName = CLS.End(xlUp).Value 'さらに離れた上を項目名とみなす
        iyS = CLS.Row 'その行からがデータとみなす
    End If
    ix = AC.Column 'データの列
    iyE = CLE.Row 'データの一番下の行
    Set CLS = Nothing
    Set CLE = Nothing
    
    '開始行
    strtemp = InputBox("データの開始行を入力してください。" & vbCrLf & "対象の列、開始行、終了行 = " & ix & ", " & iyS & ", " & iyE, "FFTグラフ作成 " & ItemName, iyS)
    If strtemp = "" Then Exit Sub
    iyS = Val(strtemp)
    
    'ポイント数の決定。2のn乗個
    inFFT = 2 ^ Int(Log(iyE - iyS + 1) / Log(2))
    iyE = iyS + inFFT - 1
    strtemp = InputBox("データ数を入力してください。" & vbCrLf & "対象の列、開始行、終了行 = " & ix & ", " & iyS & ", " & iyE, "FFTグラフ作成 " & ItemName, inFFT)
    If strtemp = "" Then Exit Sub
    inFFT = Val(strtemp)
    
    'カットオフレベルの決定
    CutOffPercent = 50
    strtemp = InputBox("元データにフィットするときのカットオフレベルを入力してください。", "FFTグラフ作成", CutOffPercent)
    CutOffPercent = Val(strtemp)
    
    'データのウィンドウ関数の選択
    strtemp = InputBox("元データに窓関数をかけますか? 0:そのまま(矩形窓) 1:Hann窓  2:Hamming窓。", "FFTグラフ作成", 0)
    iwind = Val(strtemp)
    If iwind < 0 Or iwind > 2 Then Exit Sub
    
    
    '配列の設定
    ReDim fftdataR(inFFT), fftdataI(inFFT), fftdataAbs(inFFT), fftdataOrg(inFFT), fftFreq(inFFT), DataNum(inFFT)
    ReDim fftPeriod(1 To inFFT / 2), theta(1 To inFFT / 2)
    ReDim fftdataABSperinFFT(1 To inFFT / 2), CutOffLevel(1 To inFFT / 2)
    
    For i = 0 To inFFT - 1
        DataNum(i) = i
        fftdataR(i) = Cells(iyS + i, ix) '実時間データ
        fftdataI(i) = 0#
    Next i

    fftdataOrg = fftdataR '元データを残す
    
    'FFT。時間領域データを周波数領域に変換
    FFT inFFT, fftdataR, fftdataI, iwind

    '周波数領域データの出力。周期を横軸。
    PeakLevel = 0
    For i = 1 To inFFT / 2 'i=0の直流データは使わない
        fftdataAbs(i) = Sqr(fftdataR(i) * fftdataR(i) + fftdataI(i) * fftdataI(i))
        fftFreq(i) = i / (inFFT / 2)
        fftPeriod(i) = inFFT / i '(timeunit * nnn) / i
        If PeakLevel < fftdataAbs(i) Then PeakLevel = fftdataAbs(i)
        '-1*atan()/(2pai)*T 過去が+、将来が-なので * -1
        theta(i) = WorksheetFunction.Atan2(fftdataR(i), fftdataI(i)) * 180# / pai
    Next i
    'グラフ用データ
    For i = 1 To inFFT / 2
        fftdataABSperinFFT(i) = fftdataAbs(i) / inFFT
        CutOffLevel(i) = PeakLevel * CutOffPercent / 100 / inFFT 'ピークレベルを使う
    Next i
    
    '周期グラフの作成
    chartTtl = "周期 " & ItemName & " CutLevel[%]=" & CutOffPercent
    Set objChart = plotChart(xlXYScatterLinesNoMarkers, fftPeriod, fftdataABSperinFFT, , , chartTtl) ', , ActiveCell)
    Set objChart = plotAddSeries(objChart, fftPeriod, CutOffLevel)
    objChart.Chart.Axes(xlCategory).ScaleType = xlLogarithmic
    objChart.Chart.Axes(xlCategory).LogBase = 4
    objChart.Chart.FullSeriesCollection(1).ChartType = xlXYScatterLines
    Call moveChartPosVisibleRange(objChart, 3, 3)
    
    '位相グラフの作成
    chartTtl = "位相 " & ItemName
    Set objChart = plotChart(xlXYScatterLinesNoMarkers, fftPeriod, theta, , , chartTtl) ', , ActiveCell.Offset(1, 1))
    objChart.Chart.Axes(xlCategory).ScaleType = xlLogarithmic
    objChart.Chart.Axes(xlCategory).LogBase = 4
    Call moveChartPosVisibleRange(objChart, 2, 2)
    
    '周波数領域データの出力。データのカットレベル。
    '周波数領域生データの出力
    For i = 0 To inFFT - 1
        fftdataAbs(i) = Sqr(fftdataR(i) * fftdataR(i) + fftdataI(i) * fftdataI(i))
        fftFreq(i) = i / (inFFT / 2)
    Next i

    'カットレベル以下を0にする
    For i = 0 To inFFT - 1
        If fftdataAbs(i) <= PeakLevel * CutOffPercent / 100 Then
            fftdataR(i) = 0
            fftdataI(i) = 0
        End If
    Next i
    
    'RFT。レベルカットした周波数領域のデータを時間領域に変換
    RFT inFFT / 2, fftdataR, fftdataI
    
    'レベルカット前後のデータの相関
    CorrelationCoef = CorrCoef(fftdataOrg, fftdataR, 0, inFFT - 1)
    CorrelationCoef = 100 * Round(CorrelationCoef, 3)
    
    '時間領域グラフの作成(元データとレベルカット後)
    ReDim Preserve fftdataR(inFFT - 1), fftdataOrg(inFFT - 1), DataNum(inFFT - 1)
    chartTtl = "FFT波形 " & ItemName & " CutOff " & CutOffPercent & "%" & " Corr " & CorrelationCoef & "%"
    Set objChart = plotChart(xlLine, DataNum, fftdataR, , "Filterd", chartTtl, True) ', ActiveCell.Offset(2, 2))
    Set objChart = plotAddSeries(objChart, DataNum, fftdataOrg, "Raw")
    objChart.Chart.Axes(xlCategory).ReversePlotOrder = True
    objChart.Chart.FullSeriesCollection(2).Format.Line.Weight = 0.5
    Call moveChartPosVisibleRange(objChart, 1, 1)
    
    AC.Select
    Set AC = Nothing
    Set objChart = Nothing
    Application.ScreenUpdating = True

End Sub

'逆フーリエ変換
'元の周波数領域のデータは、nnn/2を中心に左右対称(real)、左右対称かつ上下反転(imag)(nnn/2を中心点の点対称)なので
'前半部分は正負を逆転させる。縦軸は数値をデータ数nnnで割っておく。
Public Function RFT(nnn As Long, xRorg() As Double, xIorg() As Double)
    Dim xr() As Double, xi() As Double
    Dim i As Long
    ReDim xr(2 * nnn), xi(2 * nnn)
    
    'nnn/2を中心点の点対称データを作る。nnn=4なら -x(0)=単独データ, -x(1)=x(7), -x(2)=x(6), -x(3)=x(5), x(4)=中心データ
    For i = 1 To nnn
        xr(i - 1) = xRorg(i - 1) / (2 * nnn)
        xi(i - 1) = -xIorg(i - 1) / (2 * nnn)
        xr(2 * nnn - i) = xRorg(i) / (2 * nnn)
        xi(2 * nnn - i) = xIorg(i) / (2 * nnn)
    Next i
    
    'FFT実行
    FFT 2 * nnn, xr, xi
    
    '結果を元の配列に戻す
    For i = 0 To 2 * nnn - 1
        xRorg(i) = xr(i)
        xIorg(i) = xi(i)
    Next i
End Function

'参考
'http://tsuyu.cocolog-nifty.com/blog/2007/03/publi.html
'Numerical Recipes in C

'フーリエ変換。結果のf=i/(n*deltaT), 振幅=abs(x)/(n/2)
'xR実数部、xI虚数部。結果で元データを上書き。0 - nnn-1。
Public Function FFT(nnn As Long, xr() As Double, xi() As Double, Optional iwind As Long = 0)
    Dim g As Long, h As Long, i As Long
    Dim j As Double, k As Double
    Dim l As Long, m As Long, p As Long, q As Long ', n As Long, o As Long
    Dim s() As Double, c() As Double
    Dim xd As Double, a As Double, b As Double
    
    '窓関数
    If iwind <> 0 Then '1:Hann, 2:Hamming
        Call Windowing(nnn, xr, xi, iwind)
    End If
    
    's,cはポイント数の半分
    ReDim s(nnn / 2), c(nnn / 2)
    
    i = 0
    j = 0# '最後のdo loop を抜けるためにdoubleとする
    k = 0# '最後のdo loop を抜けるためにdoubleとする
    l = 0
    p = 0
    h = 0
    g = 0
    q = 0
    
    'nnn^m
    m = Log(nnn) / Log(2)
    
    'FFTの計算
    a = 0
    b = 3.14159265359 * 2 / nnn
    For i = 0 To nnn / 2
        s(i) = sin(a)
        c(i) = Cos(a)
        a = a + b
    Next i
    l = nnn
    h = 1
    For g = 1 To m
        l = l / 2
        k = 0
        For q = 1 To h
            p = 0
            For i = k To l + k - 1
                j = i + l
                a = xr(i) - xr(j)
                b = xi(i) - xi(j)
                xr(i) = xr(i) + xr(j)
                xi(i) = xi(i) + xi(j)
                If p = 0 Then
                    xr(j) = a
                    xi(j) = b
                Else
                    xr(j) = a * c(p) + b * s(p)
                    xi(j) = b * c(p) - a * s(p)
                End If
                p = p + h
            Next i
            k = k + l + l
        Next q
        h = h + h
    Next g
    j = nnn / 2
    For i = 1 To nnn - 1
        k = nnn
        If j < i Then
            xd = xr(i)
            xr(i) = xr(j)
            xr(j) = xd
            xd = xi(i)
            xi(i) = xi(j)
            xi(j) = xd
        End If
        k = k / 2
        Do While j >= k
            j = j - k
            k = k / 2
        Loop
        j = j + k
    Next i
End Function

'xR実数部、xI虚数部。結果で元データを上書き。0 - nnn-1。
'WindowFunction w(i)=a-(1-a)*cos2πi/N, i=0 to N-1
'iwind:=1 Hannning Window a=0.5, 0.5-0.5*cos2πx,0<x<1
'iwind:=2 Hammning Window a=0.54, 0.54-0.46*cos2πx,0<x<1
Function Windowing(nnn As Long, xr() As Double, xi() As Double, iwind As Long)
    Dim i As Long, alpha As Double
    Const PI = 3.14159265358979
    
    If iwind = 1 Then
        alpha = 0.5
    ElseIf iwind = 2 Then
        alpha = 0.54
    Else 'if iwind=0 then
        alpha = 1#
    End If
    
    For i = 0 To nnn - 1
        xr(i) = xr(i) * (alpha - (1 - alpha) * Cos(2 * PI * i / nnn))
        xi(i) = xi(i) * (alpha - (1 - alpha) * Cos(2 * PI * i / nnn))
    Next i
End Function

'チャートを見える範囲内に移動する。addOffsetは内側へ入れるセル数
Function moveChartPosVisibleRange(chartObj As Object, _
Optional addOffsetRow As Long = 1, Optional addOffsetCol As Long = 1)
    
    Dim VR As Range
    Dim cl As String, TL As Range, BR As Range
    Dim mvRow As Long, mvCol As Long
    Dim AC As Range
    
    Set AC = ActiveCell
    
    chartObj.Activate
    With chartObj
        '画面範囲と現在のチャートの左上、右下
        Set VR = ActiveWindow.VisibleRange
        Set TL = .TopLeftCell
        Set BR = .BottomRightCell
        '現在のチャートの左上、右下が画面範囲外にある時の移動量を決める。両方の場合右下を優先する(後で計算する)。
        If TL.Row < VR(1).Row + addOffsetRow Then
            mvRow = VR(1).Row - TL.Row + addOffsetRow
        End If
        If TL.Column < VR(1).Column + addOffsetCol Then
            mvCol = VR(1).Column - TL.Column + addOffsetCol
        End If
        If BR.Row > VR(VR.Count).Row - addOffsetRow Then
            mvRow = VR(VR.Count).Row - BR.Row - addOffsetRow
        End If
        If BR.Column > VR(VR.Count).Column - addOffsetCol Then
            mvCol = VR(VR.Count).Column - BR.Column - addOffsetCol
        End If
        'ありえない位置への移動は修正
        If TL.Row + mvRow < 1 Then mvRow = 1 - TL.Row
        If BR.Row + mvRow > Rows.Count Then mvRow = Rows.Count - BR.Row
        If TL.Column + mvCol < 1 Then mvCol = 1 - TL.Column
        If BR.Column + mvCol > Columns.Count Then mvCol = Columns.Count - BR.Column
        'チャートの左上の位置を変更
        cl = TL.Address(False, False)
        cl = Range(cl).Offset(mvRow, mvCol).Address(False, False)
        .Top = Range(cl).Top
        .Left = Range(cl).Left
    End With
    AC.Select
    
    Set AC = Nothing
    Set VR = Nothing
    Set TL = Nothing
    Set BR = Nothing
End Function


'配列からグラフを作る。参考にしたサイト。
'https://blog.goo.ne.jp/nish0707/e/8d2d2b7ec4bc52f6b23ad42d8c81af50
'http://foundknownanddone.dots2pattern.com/2016/02/excel-vba-plot-column-chart.html


'配列からグラフを作る。xlLine,xlXYScatter系
Function plotChart(ctype, xdata As Variant, ydata As Variant _
, Optional xttl As String, Optional yttl As String, Optional chartTtl As String _
, Optional bLegend As Boolean = False) ', Optional posChart As Range)
    Dim oChart As Object
    Dim AC As Range

    Set AC = ActiveCell
    'チャートオブジェクトを適当な位置で作成する
    ActiveCell.Resize(5, 5).Select
    Set oChart = ActiveSheet.ChartObjects.Add(10, 10, 300, 200)
    oChart.Activate
    With oChart
        With .Chart 'タイトル
            .ChartType = ctype 'グラフ形式を設定
            With .SeriesCollection.NewSeries
                .Values = ydata  '1軸目のy値
               If yttl <> "" Then
                   .Name = yttl
               End If
               If IsEmpty(xdata) = False Then '1軸目のx値
                   .XValues = xdata
               End If
            End With
            If bLegend = True Then
                .HasLegend = True  '凡例表示
                .Legend.Position = xlLegendPositionBottom 'グラフの下
            Else
                .HasLegend = False  '凡例表示しない
            End If

            If chartTtl <> "" Then
                .HasTitle = True
                With .ChartTitle
                    .Text = chartTtl
                    .Format.TextFrame2.TextRange.Font.Size = 11
                    .Format.TextFrame2.TextRange.Font.Bold = msoFalse
                End With
            End If
            If xttl <> "" Then
                .Axes(xlCategory, xlPrimary).HasTitle = True
                .Axes(xlCategory, xlPrimary).AxisTitle.Characters.Text = xttl
            End If
            If yttl <> "" Then
                .Axes(xlValue, xlPrimary).HasTitle = True
                .Axes(xlValue, xlPrimary).AxisTitle.Characters.Text = yttl
            End If
            .PlotArea.Format.Line.ForeColor.RGB = RGB(64, 64, 64) 'グラフ領域の枠線
        End With

    End With
    AC.Select
    
    Set plotChart = oChart
    Set AC = Nothing
    Set oChart = Nothing

End Function

'配列からグラフの線を追加する。xlLine,xlXYScatter系
Function plotAddSeries(objChart As Variant, xdata As Variant, ydata As Variant, Optional yttl As String)
    objChart.Activate
    With objChart
        'これを繰り返すと複数のラインが描ける
         With .Chart.SeriesCollection.NewSeries
            .Values = ydata  '追加軸の値
            .XValues = xdata
            If yttl <> "" Then
                .Name = yttl
            End If
         End With
    End With
    Set plotAddSeries = objChart
End Function


'x(明示なし)が同じ2つのy1(xxx),y2(yyy)の配列の部分の相関を返す
Function CorrCoef(xxx() As Double, yyy() As Double, iStart As Long, iEnd As Long) As Double
    Dim SX  As Double, SX2 As Double, SXY As Double, Sy As Double, SY2 As Double
    Dim nnn As Long
    Dim i As Long

    nnn = iEnd - iStart + 1
    For i = iStart To iEnd
        SX = SX + xxx(i)
        SX2 = SX2 + xxx(i) * xxx(i)
        Sy = Sy + yyy(i)
        SY2 = SY2 + yyy(i) * yyy(i)
        SXY = SXY + yyy(i) * xxx(i)
    Next i
    If ((nnn * SY2 - Sy * Sy) * (nnn * SX2 - SX * SX)) <> 0 Then
        CorrCoef = (nnn * SXY - SX * Sy) * (nnn * SXY - SX * Sy) / ((nnn * SY2 - Sy * Sy) * (nnn * SX2 - SX * SX))
        CorrCoef = Sqr(CorrCoef)
    Else
        CorrCoef = 0
    End If
    'Debug.Print CorrCoef
End Function