初心者のための目的志向プログラミング。PythonでGUIを作って、撮影後の画像の画角調整し、タイムラプス動画を作成するプログラムの続きです。
今回はGUIをclass構造にしてみます。pythonでclassはまだ作ったことがありませんが、それを試してみます。
前回までで複数画像に対して、バーを調整することで、パラメータを調整するUIができました。その調整値を使って、最後の動画までを作成もできるようにしました。が、global変数がそこかしこにあり、どれが残すべきパラメータかもよくわからず、いじりたい放題です。
もう少しましな形にしてみようと思います。いわゆるオブジェクト指向、クラスの知識はあることを前提としてます。が、今回はあまりオブジェクト指向な感じではないです。継承させることも考えてないですし。クラスにしただけです。
やりたいこと
画像切り出しの座標(x,y)、角度、倍率、の4つのパラメータを、GUIによる操作で取得する。任意のファイルを選び、切り出し後の複数フレームの出力画像を確認しながら操作し、最終的なタイムラプス動画を作成する。
ここまで前回完成
GUIの記述をclassにする。(機能追加ではないです。少し手段が目的になってます…。)
方針
前回ベタ書きで書いたGUIコードを、世間のサンプルプログラム、リファレンスコードでよく見る感じの、class構造にしてみる。
コード
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 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | import tkinter as tk import tkinter.filedialog import cv2 from PIL import ImageTk, Image import glob import numpy as np import make_laps WIDTH = make_laps.WIDTH HEIGHT = make_laps.HEIGHT class viewGUI(tk.Frame): #コンストラクタ(GUIレイアウト) def __init__( self , master = None ): super ().__init__(master) self .flist = [] self .img = np.zeros(( 32 , 32 , 3 ), np.uint8) self .start = np.array([ 0 , 0 , 0 , 1.0 ]) #座標x,座標y,回転角,倍率の順 self .end = np.array([ 0 , 0 , 0 , 1.0 ]) pw_main = tk.PanedWindow(master,orient = 'vertical' ) pw_main.pack(expand = True , fill = tk.BOTH) # 上側(画像表示部 表示は1/2スケール) self .canvas = tk.Canvas(pw_main, width = WIDTH / / 2 , height = HEIGHT / / 2 ) pw_main.add( self .canvas) self .image_Tk = ImageTk.PhotoImage(Image.fromarray( self .img), master = self .canvas) #表示画像の生成 # 下側(バーがゴリゴリある方) fm_ctrl = tk.Frame(pw_main) pw_main.add(fm_ctrl) # フレームにバーを配置 self .Var_scale = tk.Scale(fm_ctrl, label = '拡大率' , orient = "v" , from_ = 0.1 , to = 8.0 , resolution = 0.1 , command = self .scrl_callback) self .Var_angle = tk.Scale(fm_ctrl, label = '回転角度' , orient = "v" , from_ = - 180 , to = 180 , command = self .scrl_callback) self .Var_shifty = tk.Scale(fm_ctrl, label = '縦方向シフト量' , orient = "v" , from_ = - 1000 , to = 1000 , command = self .scrl_callback) self .Var_shiftx = tk.Scale(fm_ctrl, label = '横方向シフト量' , orient = "h" , from_ = - 1000 , to = 1000 , command = self .scrl_callback) self .Var_flist = tk.Scale(fm_ctrl, label = 'フレーム' , orient = "h" , from_ = 1 , to = len ( self .flist), command = self .img_update) self .Var_scale.pack(fill = tk.Y, side = tk.LEFT) self .Var_angle.pack(fill = tk.Y, side = tk.LEFT) self .Var_shifty.pack(fill = tk.Y, side = tk.LEFT) self .Var_shiftx.pack(fill = tk.X) self .Var_flist.pack(fill = tk.X) self .Var_scale. set ( 1.0 ) #menu bar men = tk.Menu(master) menu_file = tk.Menu(master, tearoff = 0 ) men.add_cascade(label = 'ファイル' ,menu = menu_file) menu_file.add_command(label = 'フォルダを開く' ,command = self .open_dir) menu_file.add_separator() menu_file.add_command(label = '開始位置として設定' ,command = self .set_start) menu_file.add_command(label = '終了位置として設定' ,command = self .set_end) menu_file.add_separator() menu_file.add_command(label = '名前を付けて保存' ,command = self .makelaps) master.config(menu = men) # スクロールバーが変化したときに呼ばれるコールバック関数 def scrl_callback( self ,val): tmp = self .convert_preview( self .img) w = WIDTH / / 2 h = HEIGHT / / 2 image_rgb = cv2.cvtColor(cv2.resize(tmp,(w,h)),cv2.COLOR_BGR2RGB) self .image_Tk = ImageTk.PhotoImage(Image.fromarray(image_rgb),master = self .canvas) self .canvas.create_image( 0 , 0 ,image = self .image_Tk,anchor = 'nw' ) self .canvas.create_line(w / 2 , 0 ,w / 2 ,h) self .canvas.create_line( 0 ,h / 2 ,w,h / 2 ) # こいつが回転シフト拡大を行う処理本体 def convert_preview( self ,image): h,w = image.shape[: 2 ] affine = cv2.getRotationMatrix2D((w / 2.0 , h / 2.0 ), self .Var_angle.get(), self .Var_scale.get()) affine[ 0 , 2 ] - = self .Var_shiftx.get() affine[ 1 , 2 ] - = self .Var_shifty.get() return cv2.warpAffine(image,affine,(WIDTH,HEIGHT),flags = cv2.INTER_NEAREST) # フォルダを開くアクション def open_dir( self ): fileName = tk.filedialog.askdirectory() self .flist = glob.glob(fileName + "\\*.jpg" ) self .Var_flist[ "to" ] = len ( self .flist) self .img_update( 1 ) # 画像ファイルのアップデート(フレーム変更時のコールバック関数) def img_update( self ,val): if len ( self .flist) > 0 : self .img = cv2.imread( self .flist[ int (val) - 1 ]) self .winfo_toplevel().title( self .flist[ int (val) - 1 ]) #タイトルにファイル名表示 self .scrl_callback( 0 ) # menu 開始位置を指定 def set_start( self ): self .start = np.array([ self .Var_shiftx.get(), self .Var_shifty.get(), self .Var_angle.get(), self .Var_scale.get()]) # menu 終了位置を指定 def set_end( self ): self .end = np.array([ self .Var_shiftx.get(), self .Var_shifty.get(), self .Var_angle.get(), self .Var_scale.get()]) # menu 保存 変換開始 def makelaps( self ): if len ( self .flist) > 0 : #リストが空でなければ保存 fileName = tk.filedialog.asksaveasfilename(filetypes = [( "movie file" , ".mp4" )]) if fileName ! = "": #キャンセルが押されると名前が空のようだ make_laps.makeLaps( self .flist,fileName, self .start, self .end) #変換開始 if __name__ = = '__main__' : gui = viewGUI(master = tk.Tk()) gui.mainloop() |
トータル行数は全く変わりませんでした。globalにおいてあるのは、定数扱いのWIDTHとHEIGHTだけになってます。importしているmake_laps.pyを用意(ここからコピペ)して同じフォルダにおいておけば、このコードのコピペで動くと思います。
class
宣言の記述はシンプルですね。
1 | class viewGUI(tk.Frame): |
この記述で宣言です。継承するクラスを併せて記述します。今回はtk.Frameを継承します。クラス図とか見てみたいのですが、Tkではなく、Frameを継承するスタイルが一般的なようです。リファレンスプログラムを見てもそうなっています。
基本的にはコンストラクタでもろもろ宣言、レイアウトし、イベント発生時のコールバック関数をメソッドにするような思想です。
コンストラクタ
1 | def __init__( self , master = None ) |
の記述でコンストラクタであることを示すようです。ちなみにデストラクタは
1 | def __del__( self ) |
だそうです。今回は特段後始末が思いつかないので使いませんでしたが。
コンストラクタでもろもろすべてをレイアウトしています。記述はほぼ前回のままです。後々他の関数がアクセスしたくなる変数だけ、self.をつけてメンバ変数にしています。
self.をつけるだけでメンバ変数になってしまいます。宣言という概念がないのでいまいちしっくりきませんが、そういうものだと思っておきます。
逆にコンストラクタで作りっぱなしのものはメンバ変数にはしていません。このpythonの記述ですが、何がメンバで何がメンバでないかを見分けにくいです。後で何をメンバにしたか見失いそうです。
ファイルリスト、画像、パラメータ、キャンバス、バー、これらがメンバ変数になります。ほかの関数から読み書きします。
ここでちょっと疑問なのが、pythonでの寿命関係がよくわかりません。このコンストラクタで、メニューも作っていますが、それらはメンバではないので、コンストラクタが終わったら消えそうなもんです。が、存命です。
メンバ関数(メソッド)
これらは基本何かのイベント発生時(バーが動いた時、メニューが選択された時)に呼ばれるコールバック関数になってます。中身は以前のコードのままです。globalがなくなって、selfになっただけです。
関数の引数に、メンバ関数であるおまじない(self)をつけてあげます。コールバック関数に対しても必要です。なので引数付き関数は引数が一つ増えます。
1点だけよくわからなくて調べたのが、img_update()関数で行っているタイトルバーへのファイル名設定です。これまで、globalにあったtk.Tk()のインスタンス(root)のメンバ変数titleを直接上書いてましたが、これにアクセスする手段が見つからなかったです。直感的には
1 | self .title = filename |
で触れそうですが、このAttibuteがないよ。とエラーになってしまいました。継承してるので持ってるかと思ったのですが、Frameクラスにはなさそうです。(もしくは隠蔽されている?)なので
1 | self .winfo_toplevel().title = filename |
と、関数.winfo_toplevel()を介して、titleを触る事で、解決しました。このwinfo_toplevel()では、title以外、通常root=tk.Tk()でインスタンスされたrootにある属性、例えばサイズやwindowスタイルなんかも触りに行けます。
main部
極シンプルになりました。2行です。tkのインスタンスを、今回作ったGUIクラスをに渡して、インスタンスし、おまじないのmainloop()しているだけです。
引数で渡すと同時に作られるtk.Tk()は、インスタンス化されて、どこまでが寿命なのかやっぱりよくわかりません。が動いてます。今回このtk.Tk()のインスタンスを、この後直接触る機会がなかったので、引数に直書きしました。.mainloop()の間も生きているので、いまいちスコープが分かりません。
1 | viewGUI(master = tk.Tk()).mainloop() |
これでも動いてしまいました。何が何だかわかりません。インスタンスあるの?ないの?
別にすっきりした感じもありません。行数変わってないですし。クラスになっただけです。オブジェクト指向感もないです。globalがなくなったので気分はいいです。
まとめ
Frameを継承する形のクラス構造を使って、tkのGUIを作ってみました。世にあるサンプルは基本こんな感じで紹介されてます。
・self.を付けた変数はメンバ変数
・hoge(self)でメンバ関数(メソッド)
・def __init__(self, master=None)でコンストラクタ
・self.winfo_toplevel()でwindowのトップレベルの属性にアクセス
あたりが学べました。ほんとは自分のモジュールを基底クラスとして、継承までやってみてもよかったのですが、今回そこに必然もないので考えるのをやめました。手段が目的になってしまうので。
一部寿命関係で深い理解が得られていませんが、まずはこれでGUIも完成とします。これ以上は突っ込みません。こんなのを雛形にして今後は別の画像処理で遊べそうです。
コメント