# ダウンサンプリング、FFT変換コードマニュアル/説明
## コードのアプリ化方法
Pythonコードをアプリ化する上でPyinstallerが必要なので
```
pip install pyinstaller
```
をターミナルで実行。そしてターミナルからコードを保存しているフォルダに移動し、以下を実行、約1分ほどで完了する。
```
pyinstaller --onefile --noconsole DS_and_FFT_Assembly_2.py
```
:::warning
※Macで実行すると.app,Windowsでは.exe生成されるが、Macで.exeを生成したり、Windowsで.appを生成することはできない。
:::
[Pyinstallerのオプション、ドキュメンテーション](https://pyinstaller.org/en/stable/)
実行後にコードを保存しているフォルダ内にbuild,distというフォルダが生成される。

dist内に実行ファイルがあり、クリックすると立ち上がる。

## 使い方
### 事前準備
このアプリは1つ以上のCSVが入ったフォルダを処理できる。
使用したいCSVは1つのフォルダの中にまとめる。
処理したいCSV以外はフォルダに入れない。
例:
```graphviz
digraph hierarchy {
nodesep=1.0 // increases the separation between nodes
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=dashed] //All the lines look like this
"フォルダ"->{"CSV1"}
}
```
```graphviz
digraph hierarchy {
nodesep=1.0 // increases the separation between nodes
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=dashed] //All the lines look like this
"フォルダ"->{"CSV1" "CSV2" "CSV3"}
}
```
不可な例:
```graphviz
digraph hierarchy {
nodesep=1.0 // increases the separation between nodes
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=dashed] //All the lines look like this
"フォルダ"->{"CSV1" "CSV2" "CSV3" "フォルダ2"}
"フォルダ2"->{"CSV4" "CSV5"}
}
```
```graphviz
digraph hierarchy {
nodesep=1.0 // increases the separation between nodes
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=dashed] //All the lines look like this
"フォルダ"->{"フォルダ2" "フォルダ3" "フォルダ4"}
"フォルダ2"->{"CSV1" "CSV2"}
"フォルダ3"->{"CSV3" "CSV4"}
"フォルダ4"->{"CSV5" "CSV6"}
}
```
### アプリ起動
アプリ化のセクションで生成されたdist内の実行ファイルをクリック。
:::warning
※立ち上がる際に、Macの場合はDockに一瞬Pythonが立ち上がった後にアプリが落ちたかのような挙動が見受けられるが、決して落ちたわけではなく、動いている。また、UI画面が出るまで30秒ほどかかる。
:::
立ち上がると、以下のUI画面が表示される。

Variable Name : FFT時に切り取る周波数帯の名前
Start : 切り取る周波数帯の開始周波数
End : 切り取る周波数帯の終了周波数
名前、数値全て任意のものに変更可能。
左下のRUN押下後に以下の画面に切り替わり、処理したいフォルダを選択。フォルダ、CSVについては事前準備の部分を参照。
UI画面、フォルダ選択画面にてCANCELを押下した場合、そのまま終了する。

処理が終わると、選択したフォルダ内に新しく選択したCSVの名前のフォルダが生成される

生成されたフォルダの中身。

dwn_sample.csv : 元のCSVをダウンサンプルしたもの。
EEG_Signal.png : FFT処理したシグナルのグラフ。
FFT_avg.png : FFTの平均グラフ。横軸が周波数。縦軸が
fft_res_avg.csv : ダウンサンプルしたCSVをFFT処理し、平均を出したもの。
fft_re.csv : ダウンサンプルしたCSVをFFT処理し、UI画面時に選択した周波数対ごとに切り分けたもの。
処理が全て終わると再度UI画面が表示される。他に処理するフォルダがあるなら再度実行、なければCANCELをクリックし、終了。
## コードヒエラルキー
以下の図のように*DS_and_FFT_Assembly_2.py*を呼び出し元とし、*Start_Up_Menu.py*などが呼び出し先という構造。
呼び出し元が呼び出し先を呼び、呼び出し先がタスク完了後に呼び出し元に値を返し、呼び出し元が次の呼び出し先を呼ぶといった仕組み。
呼び順は下の図の左から右。
```graphviz
digraph hierarchy {
nodesep=1.0 // increases the separation between nodes
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=dashed] //All the lines look like this
"DS_and_FFT_Assembly_2.py"->{"Start_Up_Menu.py" "Dir_Select.py" "Down_Sample.py" "FFT.py" "Freq_Selector.py"}
"Start_Up_Menu.py"->{"DS_and_FFT_Assembly_2.py"}
"Dir_Select.py"->"DS_and_FFT_Assembly_2.py"
"Down_Sample.py"->"DS_and_FFT_Assembly_2.py"
"FFT.py"->"DS_and_FFT_Assembly_2.py"
"Freq_Selector.py"->"DS_and_FFT_Assembly_2.py"
}
```
このような構造になった理由としてダウンサンプル、FFT処理、周波数ごとの切り出しを基本とし、将来的に追加したい機能があれば随時追加していくという前提なので、仮にベースが呼び元→呼び先→呼び先で、さらに呼び先を追加したい場合に呼び元→呼び先→呼び先→呼び先となり、面倒だと思ったので、今回の構造は最低限の機能かつ、呼び元→呼び先のシンプルな構造に留めた。
## 各コード解説、調整可能な変数、注意点など。
### 開発環境
筆者はMac OS, VS codeで書き、iTerm2で実行。Pythonバージョンは3.10以降を使用。3.8,3.9だとライブラリ関連の不具合が発生したので非推奨。
### DS_and_FFT_Assembly_2.py
このプログラムの呼び元。立ち上がり時にこのコードが呼ばれる。今後、呼び先を追加する場合、以下のコードを改修。
```python=
for i in range(len(dir_path)):
if dir_path[i].endswith("csv"):
print(input_file + "/" + str(dir_path[i]))
Down_Sample_Res_Path = Down_Sample.Down_sample(input_file + "/" + str(dir_path[i]))
print(Down_Sample_Res_Path)
FFT.fft_main(Down_Sample_Res_Path)
basename, sep, tail= Down_Sample_Res_Path.partition('/dwn_sample')
print(basename)
Freq_Sel_Path = basename + "/fft_res.csv"
FSM.Freq_Selector(Freq_Section,Freq_Sel_Path)
```
### Start_Up_Menu.py
アプリ立ち上がり時に表示されるUIの部分。呼ばれた時にUI画面を表示し、"RUN"をクリックされると、Variable Name, Start, Endをvalueという変数に以下のように格納し、呼び出し元へ返す。
```
['Delta', '0.5', '2.75', 'Theta', '3.5', '6.75', 'Alpha1', '7.5', '9.25', 'Alpha2', '10', '11.75', 'Beta1', '13', '16.75', 'Beta2', '18', '29.75', 'Gamma1', '31', '39.75', 'Gamma2', '0', '0']
```
UI変更には以下を改修。
```python=
layout = [[PSG.Text("EEG Range",size = (10,2), font = ('Helavecta',20),justification='left')],
[PSG.Text("Variable Name",size = (20,2), font = ('Helavecta',20),justification='center'),
PSG.Text("Start",size = (20,2), font = ('Helavecta',20),justification='center'),
PSG.Text("End",size = (20,2), font = ('Helavecta',20),justification='center')],
[PSG.Input("Delta",size = (18,1),justification="left",font = ('Helavecta',20)),
PSG.Text(':', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("0.5",size = (18,1),justification='center',font = ('Helavecta',20),),
PSG.Text('~', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("2.75",size = (18,1),justification='center',font = ('Helavecta',20),)],
[PSG.Input("Theta",size = (18,1),justification="left",font = ('Helavecta',20)),
PSG.Text(':', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("3.5",size = (18,1),justification='center',font = ('Helavecta',20),),
PSG.Text('~', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("6.75",size = (18,1),justification='center',font = ('Helavecta',20),)],
[PSG.Input("Alpha1",size = (18,1),justification="left",font = ('Helavecta',20)),
PSG.Text(':', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("7.5",size = (18,1),justification='center',font = ('Helavecta',20),),
PSG.Text('~', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("9.25",size = (18,1),justification='center',font = ('Helavecta',20),)],
[PSG.Input("Alpha2",size = (18,1),justification="left",font = ('Helavecta',20)),
PSG.Text(':', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("10",size = (18,1),justification='center',font = ('Helavecta',20),),
PSG.Text('~', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("11.75",size = (18,1),justification='center',font = ('Helavecta',20),)],
[PSG.Input("Beta1",size = (18,1),justification="left",font = ('Helavecta',20)),
PSG.Text(':', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("13",size = (18,1),justification='center',font = ('Helavecta',20),),
PSG.Text('~', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("16.75",size = (18,1),justification='center',font = ('Helavecta',20),)],
[PSG.Input("Beta2",size = (18,1),justification="left",font = ('Helavecta',20)),
PSG.Text(':', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("18",size = (18,1),justification='center',font = ('Helavecta',20),),
PSG.Text('~', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("29.75",size = (18,1),justification='center',font = ('Helavecta',20),)],
[PSG.Input("Gamma1",size = (18,1),justification="left",font = ('Helavecta',20)),
PSG.Text(':', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("31",size = (18,1),justification='center',font = ('Helavecta',20),),
PSG.Text('~', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("39.75",size = (18,1),justification='center',font = ('Helavecta',20),)],
[PSG.Input("Gamma2",size = (18,2),justification="left",font = ('Helavecta',20)),
PSG.Text(':', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("0",size = (18,2),justification='center',font = ('Helavecta',20),),
PSG.Text('~', size = (3,1),justification='center',font = ('Helavecta',20)),
PSG.Input("0",size = (18,2),justification='center',font = ('Helavecta',20),)],
[PSG.Button("RUN", font=("Helvecta",20)),PSG.Button("CANCEL", font=("Helavecta",20))]
]
```
### Dir_Select.py
呼び出し元が上記のStart_Up_Menu.pyを呼び出し終わると呼ばれる。
フォルダを選択し、選択したフォルダのパスを呼び出し元へ送る。
コードは現在フォルダ選択にtkinterを使っているが、どうやらPytonアプデなどで度々不具合を起こしたり、現在デフォルトでPythonに含まれているはずが、コードを走らせると*modlue not found*エラーが出たりとお世辞にも安定しているように見えないので、将来的に他のライブラリに変更することを推奨。
```python=
def get_dirname():
# Dir選択ダイアログの表示
root = tkinter.Tk()
root.withdraw()
dir_path = tkinter.filedialog.askdirectory()
return dir_path
```
### Down_Sample.py
呼び出し元がDir_Select.pyを呼び出し終わると呼ばれる。
Dir_Select.pyで得たフォルダ内のCSVパスからCSVを読み込み、
600Hzのデータを500Hzにダウンサンプルし、*XXX/dwn_sample.csv*というファイルを生成。
最後に呼び出し元に生成したCSVファイルのパスを返す。
現状、使うCSVはSIRUSではなく、アプリから取得したものを使う前提なので、今後違うソースからのCSVを使う場合は以下コードの使う列の指定を変更(この場合*brainWave*)を正しいものに変更したり、9行目の計算式をそれぞれ使うCSVに合わせる必要がある。
```python=
def Down_sample(input_file: str):
# input_file = GUI.get_filename()
# print("initial inut file",input_file)
# basename = os.path.basename(input_file).split(".",1)[0]
basename, sep, tail= input_file.partition('.')
df_1 = pd.read_csv(input_file, usecols = ['brainWave'], sep = ',')
#Dataframe to float conversion
num = pd.to_numeric(df_1['brainWave'])
num = num * 18.3 / 64
```
また、違う周波数をダウンサンプリングしたり、逆にアップサンプリングする場合は下記の部分を変更。
```python=
#Writes converted data to csv
j = 0 # j:Sample Number (600Hz)
res_out.write(str(0) + ',' +str(num[0]) + "\n")
for i in tqdm(range(1, int((len(num)*(1/600))/(1/500)))): # i:Sample Number (500Hz)
if (i/500 > (j+1)/600): # j/600 < i/500 <= (j+1)/600
j += 1
a = LinearFunc(1/600, num[j], 2/600, num[j+1])
y = num[j] + a*(i/500-j/600)
res_out.write(str(i/500) + ',' +str(y) + "\n")
j += 1
```
### FFT.py
呼び出し元がDown_Sample.pyを呼び出し終わると呼ばれる。
呼び出し元から上記のダウンサンプリングで生成されたCSVのパスを渡され、FFT処理。
処理後、*XXX/fft_res_avg.csv*、*XXX/fft_res.csv*というCSVを2つ生成、fft_resのパスを呼び出し元に返す。
以下のパワースペクトラムに関わる部分は
ウィンドウサイズが3秒、周波数リミットが40にセットしてあるが、必要に応じて変更。
現在は*def power_spectrum*を使用、以前使っていた*def fft_ps*は使っていないが、将来使うかもしれないのでキープ。
```python=
def power_spectrum(sig: np.ndarray, fs: int, win_sec: int = 3, fq_lim: int = 40) -> np.ndarray:
# Spectrum Analysis (Short-time Fourier Transform & Power Spectrum)
# Input:
# - sig: EEG Signal [μV]
# - fs: Sampling Rate [Hz]
# - win_sec: Short-time Fourier Transformation Window Size [sec]
# - fq_lim: Upper Limitation Frequency [Hz]
# Output:
# - spctgram: Spectrogram
# - freq: Frequency Label
# - t: Timeseries Label
# Parameter Setting
dt = 1. / fs # Sampling Interval [sec]
n = fs * win_sec # Data Length [points]
nfft = 2 ** nextpow2(n) # next Power of 2
df = 1. / (nfft*dt) # Frequency Interval [/sec]
# print("Parameters...")
# print("Sampling Intervals: ", dt)
# print("Data Length: ",n)
# print("Next Power of 2: ", nfft)
# print("Freq Interval: ", df)
# print("Window Size: ", win_sec)
# Spectrogram (Short-time Fourier Transformation)
freq, t, spctgram = signal.spectrogram(sig, fs=fs, nperseg=n, nfft=nfft, noverlap=(win_sec-1)*fs, window='hamming', mode='psd', return_onesided=True)
freq = freq[:math.ceil(fq_lim/df)]
spctgram = np.abs(spctgram[:math.ceil(fq_lim/df),:])
#db = 10 * np.log10(spctgram)
return spctgram, freq, t
```
```python=
def fft_ps(sig: np.ndarray, fs: int, fq_lim: int = 40):
# Spectrum Analysis
# Input:
# - sig: EEG Signal [μV]
# - fs: Sampling Rate [Hz]
# - Nt: Data Length [sec]
# Output:
# - ps: Power Spectrum
# - fq: Frequency Label
# Parameter Setting
dt = 1./fs # Sampling Interval [sec]
n = len(sig) # Data Length [points]
nfft = 2 ** nextpow2(n) # next Power of 2
df = 1./(nfft*dt) # Frequency Interval [/sec]
# Fast Fourier Transformation
#window = np.hamming(n) # hanning window
window = signal.hamming(n) # hamming window
sig_window = sig * window
acf = 1 / (sum(window)/n) # Amplitude Correction Factor
acf *= (sum(np.abs(sig_window))/n) / (sum(np.abs(sig_window))/nfft) # Amplitude Correction Factor
amp = fftpack.fft(sig_window, nfft) / (nfft/2) # Amplitude
fq = fftpack.fftfreq(nfft, d=dt) # Frequency
amp = amp[:math.ceil(fq_lim/df)]
fq = fq[:math.ceil(fq_lim/df)]
ps = (acf*np.abs(amp)) ** 2 # Power Spectrum [V**2]
psd = ps / df # Power Spectrum Density [V**2/Hz]
return ps, fq
```
### Freq_Selector.py
呼び出し元がFFT.pyを呼び出し終わると呼ばれる。
呼び出し元から上記のFFTで生成されたCSVのパスを渡され、UI画面で入力した周波数帯ごとに切り分け。
*XXX/Freq_partition.csv*を生成。