デジタルホログラフィーの処理をPythonで書いてみた

デジタルフォログラフィーの画像処理、計算をPython3.7で書いたものです。
ほぼ自分でやったことをメモっておく用
Win10で開発、テストしました。Macだとクラッシュします。
## やったこと
１．TkinterでのGUI作成(ボタン、入力欄、チェックボックス)
２．画像を読み込んでトリミング
３．トリミングした画像を行列に変換
４．参照光を当てて（計算）して再生
５．再生像の画像を生成して保存、表示
##出来上がったもの
<img width="956" alt="5cdaee82f2f99d22f51da20321a364c8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352326/e3d429f1-5911-e7b0-9bbf-91154e0fc25f.png">
##使用したライブラリ
```python:Digital_Holo.py
import sys
import tkinter as tk
import tkinter.filedialog as fd
from PIL import Image, ImageTk
import numpy as np
from numpy.fft import fftshift, ifftn
import matplotlib.pyplot as plt
import cv2
```
##TkinterでのGUI作成
Tkinterについてはここによくまとまっています。
[Python3 Programming](http://www.nct9.ne.jp/m_hiroi/light/python3.html)
###ウィンドウのみ
```python:Digital_Holo.py
root = tk.Tk()
root.title("Digital Hologram")
root.geometry("1280x720")
root.mainloop()
```
これで上の画像のボタン等が一切ないものができる。
ボタン等を設置するコードはroot.mainloop()の前に書けばよい。

###文章を書く

```python:Digital_Holo.py
lb = tk.Label(root,text="扱える画像は.bmpのみです")
lb.pack()
lb.place(x=20,y=20)
```
これで文章が書ける。
###ボタン作成
```python:Digital_Holo.py
#画像選択開始用ボタン
Button_1 = tk.Button(text=u"select image")
Button_1.bind("<Button-1>",flag_image_processing)
Button_1.pack()
Button_1.place(x=50,y=50)
#画像選択後、処理開始用ボタン
Button_2 = tk.Button(text=u"make Hologram")
Button_2.bind("<Button-1>",flag_Hologram)
Button_2.pack()
Button_2.place(x=50,y=200)
```
Button関数を使えば文章を書くのと同様に作れる。ボタンに文章を書くこともできる。重要なのは特定の操作(ここでは左クリック)をフラグとして関数を呼び出せるところ。関数の中身は後述する。
###入力欄
```python:Digital_Holo.py
#距離入力用
e_1 = tk.Entry(root, textvariable = d)
e_1.pack()
e_1.focus_set()
e_1.bind("<Return>", Enter_distance)
e_1.place(x=50,y=170)
```
ここも同様。textvariableで指定した変数に入力された値をstr型として格納することが出来る。ボタンと同様に
特定の操作(ここではEnterキー)をフラグとして、関数を呼び出せる。
e_1.get()で格納された値を取得できる。
###チェックボックス
```python:Digital_Holo.py
option_1 = tk.BooleanVar()
option_1.set(True)
Check_1 = tk.Checkbutton(text = "処理終了時に画像を表示", variable=option_1)
Check_1.pack()
Check_1.place(x=170,y=200)
```
チェックボックスも同様に作成できる。チェックが入っているかどうかを、BooleanVar関数で作成した変数にTrue or Falseの二値で格納できる。画像を表示するかどうかの際に使用する。入力欄と同様にoption_1.get()でチェックボックス内の値を取得できる。
###ボタンを押したときの処理
```python:Digital_Holo.py
def flag_image_processing(event):
    if(f==1):
        lbl_6.destroy()
        lbl_7.destroy()
        lbl_8.destroy()
    global file_name
    file_name, image = load_file()
    global image_sq
    image_sq = trimming(image,file_name)

def load_file():
    #filepathをフルパスで取得
    #file_name = fd.askopenfilename(filetypes = [("Image Files", (".bmp",".jpg"))]) テスト用
    file_name = fd.askopenfilename(filetypes = [("Image Files", (".bmp"))])
    #jpgはテスト用で使うと画像がリサイズされるので注意
    #PILを使って画像を取得
    image = Image.open(file_name)
    print(image)
    print("load is done!")
    m_1 = "{}をロードしました"
    lbl_6 = tk.Label(root,text=m_1.format(file_name))
    lbl_6.pack()
    lbl_6.place(x=530,y=80)
    f = 1
    return file_name, image

def trimming(x,y):
    #画像をx軸で324~2268、y軸で0~1944までトリミングし保存
    imagesq = x.crop((324,0,2268,1944))
    newFile = y.replace(".bmp","_sq.png")
    imagesq.save(newFile)
    m_2 = "{}をトリミングして{}として保存しました"
    lbl_7 = tk.Label(root,text=m_2.format(y,newFile))
    lbl_7.pack()
    lbl_7.place(x=530,y=110)
    return imagesq
```
上のボタンを押したときの処理。
askopenfilename関数で選択した画像のフルパスを取得し、Image.openで画像をロード、trimming関数に渡して
トリミングした画像を拡張子を変えて保存という流れ。
PhotoImageという画像を読み込むTkinterの関数があるが、こちらは扱える拡張子がGIFとPGM/PPMに限られている。
二回目以降は、以前の進捗の文章を消さなければならないので最初にdestroy()を書く。ここはもう少し改良できそう。

```python:Digital_Holo.py
def flag_Hologram(event):
    global theImage
    theHologram = np.array(image_sq, np.float) #numpyで処理するためndarrayに変換する
    theHologram =complex(theHologram)
    wave_length =632.8E-9
    dx =2.2E-6
    pixel =range(1,1945)
    pixel =float(pixel)
    KL = list()
    K = -1j*np.pi/wave_length/distance*(pixel**2*dx**2)
    L = -1j*np.pi/wave_length/distance*(pixel**2*dx**2)
    for i in range(1944):
        KL.append(np.exp(K[i])*np.exp(L))
    KL = np.array(KL)
    C = fftshift(ifftn(KL*theHologram))
    theImage = np.log(abs(C))
    display_Fourier(theImage,file_name)

def display_Fourier(data,name):
    """
    imshow()はndarrayからの変換には対応していない模様
    plt.imshow(data, cmap="Greys")
    plt.show()
    """
    im = cv2.flip(data * j, 0)
    im = Image.fromarray(np.uint8(im))
    name = name.rstrip(".bmp")
    newfile = name + "_dist=" + str(distance) + "_Holo.png"
    im.save(newfile)
    m_3 = "再生像を{}として保存しました"
    lbl_8 = tk.Label(root,text=m_3.format(newfile))
    lbl_8.pack()
    lbl_8.place(x=530,y=170)
    print("save is done!")
    if(option_1.get()==True):
        display(im,newfile)

def display(file,name):
    #画像を表示
    sub_window = tk.Toplevel()
    sub_window.title("Result")
    sub_window.geometry("600x600")
    image = Image.open(name)
    image = image.resize((600,600))
    photo = ImageTk.PhotoImage(image,master=sub_window)
    label_1 = tk.Label(sub_window,image=photo)
    label_1.image = photo
    label_1.pack()
```
下のボタンを押したときの処理。
トリミングした画像を行列に変換し、画像と同サイズの参照光の行列を作成、多次元逆高速フーリエ変換を行い、
display_Fourierに関数に渡し行列から画像を生成、ファイル名を変えて保存するという流れ。
チェックボックスの値を取得しチェックされているなら、生成した画像を別ウィンドウに表示するようになっている。
行列は型はndarrayで、このままだと画像に変換できないためuint8に変換する。画素値が0.0~1.0のときに型変換のときに
255で乗算するが今回は、0.0~1.0でないがそのままだと画素値が低すぎて出力がうまくいかないのでとりあえず20としている。後にでてくるテスト用関数はここ絡みのもの。
画像はそのまま表示すると大きすぎるので600x600にリサイズされている。この画像出力部分が曲者で制約が多い。
特に起動時に生成されるウィンドウ以外の部分に画像を出力するときには、上以外のやり方だとうまくいかないことが多い。
###その他の関数
```python:Digital_Holo.py
def Enter_distance(event):
    global distance
    distance = e_1.get()
    m_4 = "物体とカメラの距離を{}(m)として計算します"
    lbl_9 = tk.Label(root,text=m_4.format(distance))
    lbl_9.pack()
    lbl_9.place(x=100,y=170)
    lbl_9.pack_forget()
    distance = float(distance)
```
カメラと物体の距離入力用関数。ボタンを押されたときに呼び出される関数でget関数を使って入力された数字を読み取り、後で計算するためfloat形に変換する単純なもの。
pack_forget()を最後に書かないと、二回目以降の入力でエンターを押さなくても更新されたように表示されてしまうので注意。

```python:Digital_Holo.py
def Figure(event):
    global j
    j = e_2.get()
    j = int(j)
```

上と同じ。画像出力されるときの調整用。

```python:Digital_Holo.py
def display_Fourier_test(data,name):
    #画像出力のテスト用です、多分使わないと思う
    name_a = name
    for i in range(50):
        im = cv2.flip(data*i, 0)
        im = Image.fromarray(np.uint8(im))
        name = name_a
        name = name.replace(".bmp","a")
        name = name.rstrip("a")
        name = name + str(i)
        newfile = name + ".png"
        im.save(newfile)
```

上の関数で入力する数字を求めるための関数。多分使わないと思う。
##numpy.vectorizeについて
```python:Digital_Holo.py
complex =np.vectorize(complex)
float = np.vectorize(float)
```
pythonの関数で引数にリストを使う場合、これをやらないとだめらしい。詳しくはこちら→[numpy.vectorizeの使い方](https://qiita.com/3x8tacorice/items/3cc5399e18a7e3f9db86)
##コード
[Github](https://github.com/AIMU24601/Digital_Holo)
##参考にしたサイト
[Python3 Programming](http://www.nct9.ne.jp/m_hiroi/light/python3.html)
[numpy.vectorizeの使い方](https://qiita.com/3x8tacorice/items/3cc5399e18a7e3f9db86)
[OpenCV-Python Tutorials 1 documentation](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_transforms/py_fourier_transform/py_fourier_transform.html)
[Python, NumPyで画像処理（読み込み、演算、保存）](https://note.nkmk.me/python-numpy-image-processing/)
[The Tkinter PhotoImage Class](https://effbot.org/tkinterbook/photoimage.htm)