フーリエ変換をする際、漏れ誤差の改善のために一般的に窓関数を用います。ここではiOSアプリ開発でも窓関数の適用ができるようSwiftによる窓関数の適用方法を紹介し、実際に時間波形に対して適用、グラフ表示まで行います。
こんにちは。wat(@watlablog )です。ここではSwiftUIを使った信号処理コードシリーズとして窓関数の実装を紹介します !
窓関数のおさらいWATLABブログはPythonに関する情報発信を基本としていましたので、窓関数については以下の記事でそれぞれ紹介をしていました。この記事でも窓関数の概要は説明しますが、筆者と同じくPythonの方が得意な読者はこちらの記事を是非参照してみてください。 ・PythonでFFT!SciPyで窓関数をかける ・Pythonで窓関数が無い場合は?指数窓を自作してみる! ・窓関数使用時の補正!FFTの時に忘れがちな計算とは?
いつも正しい周期で波形を切り出して分析できれば良いのですが、実際は様々な周波数成分の信号に対して全てキリよく抽出することができません。中途半端な信号をフーリエ変換した場合に振幅スペクトルが分散してしまう漏れ誤差 (Leakage error )が発生してしまいます。これを軽減するために窓関数 が使われます。
概要の説明はPython記事に任せて、この記事では早速SwiftUIでコーディングを始めたいと思います!
SwiftUIで窓関数を実装する 動作環境本記事のコードは以下の環境で動作を確認しました。
Mac
OS macOS Ventura 13.2.1 CPU 1.4[GHz]
メモリ 8[GB]
Xcode
Version 14.3(14E222b)
Swift
Version 5.8(swiftlang-5.8.0.124.2 clang-1403.0.22.11.100)
iPhone SE2
OS iOS 16.3.1
ハニング窓まずは最も使用頻度の高いハニング窓(通称ハン窓)を書いてみます。ハニング窓は式(1)で示されます。は窓関数計算後の波形です。ハニング窓がよく使われる理由としては、時間波形に含まれる振動数と振幅をともに精度よく分析できるからだそうです。
Swiftの関数で書いたコードがこちらです。データ点数Nを引数とし、ハニング窓をかけた後の波形を返す関数にしてみました。
func hanningWindow ( N : Int ) -> [ Float ] {
// ハニング窓
let w = ( 0 .. < N ) . map { 0.5 - 0.5 * cos ( 2.0 * . pi * Float ( $ 0 ) / Float ( N - 1 ) ) }
return w
}
GUIとしてChartにグラフ表示するところまで書いた全コードを以下に示します。このコードはwavePoints 関数で理想的な正弦波を作ってChartに表示させるプログラムに対して、間に窓関数の適用を挟んだ構成で書いています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import SwiftUI
import Charts
import Accelerate
struct PointsData : Identifiable {
// 点群データの構造体
var xValue : Float
var yValue : Float
var id = UUID ( )
}
struct ContentView : View {
// データを定義
@ State private var data : [ PointsData ] = [ ]
@ State private var x : [ Float ] = [ ]
@ State private var y : [ Float ] = [ ]
@ State private var showingAlert = false
func linspace ( start : Float , stop : Float , num : Int ) -> [ Float ] {
// データ点数を指定して配列を作成する関数
let increment = ( stop - start ) / Float ( num - 1 )
return ( 0 .. < num ) . map { Float ( $ 0 ) * increment + start }
}
func wavePoints ( N : Int , length : Float ) -> ( xArray : [ Float ] , yArray : [ Float ] ) {
// プロットするデータを演算する関数
// データ点数Nと時間長lengthからx軸を作成
let xArray = linspace ( start : 0.0 , stop : length , num : N )
// 正弦波を作成。A:振幅、f:周波数[Hz]、dc:直流成分
let A : Float = 1
let f : Float = 10
let dc : Float = 0
let yArray = xArray . map { A * sin ( 2 * . pi * f * $ 0 ) + dc }
return ( xArray , yArray )
}
func hanningWindow ( N : Int ) -> [ Float ] {
// ハニング窓
let w = ( 0 .. < N ) . map { 0.5 - 0.5 * cos ( 2.0 * . pi * Float ( $ 0 ) / Float ( N - 1 ) ) }
return w
}
var body : some View {
// UI
VStack {
Chart {
// データ構造からx, y値を取得してプロット
ForEach ( data ) { shape in
LineMark (
x : . value ( "x" , shape . xValue ) ,
y : . value ( "y" , shape . yValue )
)
}
}
. padding ( . all , 10 )
Button ( "Button" ) {
// ボタンイベント
// 関数を実行してx, y演算結果を取得
let result = wavePoints ( N : 500 , length : 1 )
let x = result . xArray
var y = result . yArray
// ハニング窓関数を適用
let window = hanningWindow ( N : x . count )
y = zip ( y , window ) . map ( * )
// プロットデータの全削除
data . removeAll ( )
// プロットデータの追加
data . append ( contentsOf : zip ( x , y ) . map { PointsData ( xValue : $ 0 , yValue : $ 1 ) } )
}
}
}
}
struct ContentView_Previews : PreviewProvider {
static var previews : some View {
// プレビュー
ContentView ( )
}
}
下図が実行結果です。Button という文字をクリックすると両端が0になった波形が表示されます。
フラットトップ窓続いてよく使われる窓関数の例として、もう一つフラットトップ窓 を紹介します。フラットトップ窓は式(2)で示されます。この窓関数は周波数分解能を犠牲にして振幅成分をより精密に分析する時に使います。
こちらが関数です。一度に式を書いたら「The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions 」というメッセージが出たので、いくつかの式に分割して最後にまとめるという方法で書いてみました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func flatTopWindow ( N : Int ) -> [ Float ] {
// フラットトップ窓関数
let a0 : Float = 1.0
let a1 : Float = 1.93
let a2 : Float = 1.29
let a3 : Float = 0.388
let a4 : Float = 0.032
let w = ( 0 .. < N ) . map { n in
let term1 = a0
let term2 = a1 * cos ( 2 * . pi * Float ( n ) / Float ( N - 1 ) )
let term3 = a2 * cos ( 4 * . pi * Float ( n ) / Float ( N - 1 ) )
let term4 = a3 * cos ( 6 * . pi * Float ( n ) / Float ( N - 1 ) )
let term5 = a4 * cos ( 8 * . pi * Float ( n ) / Float ( N - 1 ) )
return term1 - term2 + term3 - term4 + term5
}
return w
}
以下に実行結果を示します。中心付近の振動成分がより目立つような形になりました。
まとめ本日はここまで。Swift系の記事は一度Pythonでやった内容を書き直すような作業であるため簡単にしています。とはいえ、Pythonでは窓関数もライブラリを使って適用していたのに対し、Swiftでは自分で式を組み込むということをしてみました。
窓関数の式の違いにより周波数波形にどう違いが出てくるかといった細かい話はいずれどこかでまとめるかも知れません。
参考文献
[1]:永井健一, 丸山真一, システム計測工学 ポイントでわかる機械計測の基礎と実践, 森北出版, 2011, pp112-115
Swiftでも窓関数を実装するやり方がわかりました! Twitter(今はXと呼ぶ?)でも関連情報をつぶやいているので、wat(@watlablog )のフォローお待ちしています!