初心者のための目的志向プログラミング。以前の記事でタイムラプス動画をPython使って、連番JPEGから作る方法をステップを追って書いてみました。ただこのプログラムですが、完全にスクリプトの状態で、各種パラメータはプログラムベタ書きの、CUIベースのプログラムになっています。
そこで、今回も勉強がてらPythonでGUIを作って、画角の調整をするプログラムを何回かに分けて、作ってみようと思います。
tkinterの導入
簡単なGUIを作るライブラリとしてtkinterというライブラリがPythonには用意されています。以前の記事で画像表示の1手段としてのこれを書いたことがありますが、これを発展させて、画角の調整用バーを加え、プログラムベタ書きだったパラメータを調整しやすくする仕組みを考えます。
やりたいこと
以前のスクリプトであらかじめベタで指定することになっていた、画像切り出しの座標(x,y)、角度、倍率、の4つのパラメータを、GUIによる操作で取得する。
切り出し後の出力画像を確認しながら操作する。
方針
tkinterのCanvasに画像を表示しながら、4本のScale(バー)を操作し、その操作結果をパラメータとしてダイナミックに画像の切り出し処理に反映させ、パラメータを決定する。
スクロール変化のコールバックを受けて、以前書いたタイムラプス作成プログラムで作った関数をコールし、画像の変換を行う。
コード
はじめの一歩的には、以前作成した画像表示プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import cv2 import tkinter as tk from PIL import ImageTk, Image img = cv2.imread( 'lena2.jpg' ) root = tk.Tk() canvas = tk.Canvas() image_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) image_pil = Image.fromarray(image_rgb) image_Tk = ImageTk.PhotoImage(image_pil, master = canvas) canvas.create_image( 0 , 0 ,image = image_Tk,anchor = 'nw' ) canvas.grid() root.mainloop() |
これですが、このコードにScaleを4本追加して、その変化のコールバックを受ける関数を作ってみただけです。
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 | import tkinter as tk import cv2 from PIL import ImageTk, Image WIDTH = 1920 HEIGHT = 1080 FILE = 'Photo_0001.jpg' # スクロールバーが変化したときに呼ばれるコールバック関数 def scrl_callback(val): tmp = convert_view(img) w = WIDTH / / 2 h = HEIGHT / / 2 image_rgb = cv2.cvtColor(cv2.resize(tmp,(w,h)),cv2.COLOR_BGR2RGB) global image_Tk image_Tk = ImageTk.PhotoImage(Image.fromarray(image_rgb),master = canvas) canvas.create_image( 0 , 0 ,image = image_Tk,anchor = 'nw' ) canvas.create_line(w / 2 , 0 ,w / 2 ,h) canvas.create_line( 0 ,h / 2 ,w,h / 2 ) # こいつが回転シフト拡大を行う処理本体 def convert_view(image): h,w = image.shape[: 2 ] affine = cv2.getRotationMatrix2D((w / 2.0 , h / 2.0 ),Var_angle.get(),Var_scale.get()) affine[ 0 , 2 ] - = Var_shiftx.get() affine[ 1 , 2 ] - = Var_shifty.get() return cv2.warpAffine(image,affine,(WIDTH,HEIGHT),flags = cv2.INTER_NEAREST) root = tk.Tk() img = cv2.imread( FILE ) pw_main = tk.PanedWindow(root,orient = 'vertical' ) pw_main.pack(expand = True , fill = tk.BOTH) # 上側(画像表示部 表示は1/2スケール) canvas = tk.Canvas(pw_main, width = WIDTH / / 2 , height = HEIGHT / / 2 ) pw_main.add(canvas) image_Tk = ImageTk.PhotoImage(Image.fromarray(img), master = canvas) #表示画像の生成 # 下側(バーがゴリゴリある方) fm_ctrl = tk.Frame(pw_main) pw_main.add(fm_ctrl) # フレームにバーを配置 Var_scale = tk.Scale(fm_ctrl, label = '拡大率' , orient = "v" , from_ = 0.1 , to = 8.0 , resolution = 0.1 , command = scrl_callback) Var_angle = tk.Scale(fm_ctrl, label = '回転角度' , orient = "v" , from_ = - 180 , to = 180 , command = scrl_callback) Var_shiftx = tk.Scale(fm_ctrl, label = '横方向シフト量' , orient = "h" , from_ = - 1000 , to = 1000 , command = scrl_callback) Var_shifty = tk.Scale(fm_ctrl, label = '縦方向シフト量' , orient = "v" , from_ = - 1000 , to = 1000 , command = scrl_callback) Var_scale. set ( 1.0 ) Var_scale.pack(fill = tk.Y, side = tk.LEFT) Var_angle.pack(fill = tk.Y, side = tk.LEFT) Var_shifty.pack(fill = tk.Y, side = tk.LEFT) Var_shiftx.pack(fill = tk.X) scrl_callback( 0 ) root.mainloop() |
解説
今回はmain関数は用意しませんでした。コールバックの関数がglobalの変数を触る構成なので、mainの中に変数を置いておくと触れないからです。global変数使いたくはないんですけどね。classとかにした方がかっこいいかもしれません。
まず先頭の
1 2 3 | WIDTH = 1920 HEIGHT = 1080 FILE = 'Photo_0001.jpg' |
は定数。切り出し後の画像サイズと扱う画像ファイルを指定しています。
処理の本流は以下から開始。
1 2 | root = tk.Tk() img = cv2.imread( FILE ) |
でTkのオブジェクトを作って、置いときます。半分お約束です。同様に画像も読んで置いときます。画像変換の関数がOpenCVなので、開いて置いておくのもOpenCVを使います。
1 2 | pw_main = tk.PanedWindow(root,orient = 'vertical' ) pw_main.pack(expand = True , fill = tk.BOTH) |
でPanedWindowを用意、適用(pack)します。この適用時に調整項目を入れられますが、今回は、拡大可能(expand=True)で、縦横拡大(fill=tk.BOTH)にします。これには今後CanvasとFrameをレイアウトします。そのFrameにScaleを置く構成にします。
ウィンドウとフレームとウィジェットというキーワードが出てきます。概念としては、ウィンドウの中には複数のフレームがレイアウトでき、そのフレームの中には複数のウィジェット(画像とかボタンとかラベルとか)がレイアウトできるようです。ウィンドウに直接ウィジェットを置くこともできそうです。なので今回は

な構成になります。
ウィンドウにCanvasウィジェットとFrameを置きます。そのFrameに4本のScaleウィジェットを置きます。
1 2 3 | # 上側(画像表示部 表示は1/2スケール) canvas = tk.Canvas(pw_main, width = WIDTH / / 2 , height = HEIGHT / / 2 ) pw_main.add(canvas) |
で先のWindowにCanvasウィジェットをレイアウトします。表示用の画像は切り出し画像の1/2のサイズにします。「//」で割り算結果を整数にすることができるようです。ここではPanedWindowに対してaddを使います。この辺りがよくわかりません。(canvas.packとかgridではダメ)そもそも第一引数が何を目的に親Windowをしてしているのかよくわかっていません。リファレンスを見てもよくわからなかった。
1 2 3 | # 下側(バーがゴリゴリある方) fm_ctrl = tk.Frame(pw_main) pw_main.add(fm_ctrl) |
同様にFrameをレイアウトします。このFrameに今度はScaleウィジェットをレイアウトします。
1 2 3 4 | # フレームにバーを配置 Var_scale = tk.Scale(fm_ctrl, label = '拡大率' , orient = "v" , from_ = 0.1 , to = 8.0 , resolution = 0.1 , command = scrl_callback) Var_scale.pack(fill = tk.Y, side = tk.LEFT) Var_scale. set ( 1.0 ) |
1本のバーを代表に説明します。これを4回繰り返しているだけです。(微妙にバーの整数型、少数型の違いや、レイアウトの水平、垂直の違い等ありますが)
スケールウィジェット(tk.Scale)でバーを作ります。それをframeに対してpackしてやります。 設定は最低限の項目で済ませています。ラベルや向き、最小値、最大値、バーの刻み、そしてコールバック関数を設定しています。
最後にレイアウトする位置を決定しつつ配置(pack)しています。 このpackの際に、横引き延ばし(tk.X)なのか、縦引き延ばし(tk.Y)なのか、をfillで指定してやり、レイアウトする位置をsideで指定してあげます。
ここで、開始終了の値は固定値を取ってます。縦横のシフト画素数は入力の画像によってはここで設定している±1000では足りないこともあるので、そこは適宜変更の必要があるかと思います。(入力画像に応じて変化させてもよいかもしれません)
最後に初期値の1.0倍をセット(.set())しています。 バーの初期値が0に一番近い正の値のようです。x,yのシフト量、角度は0がスタートで自然なのですが、倍率が最小値の0.1倍スタートになってしまってます。 なので、倍率のバーだけ、1.0をセットしています。
続いてコールバックされる関数(バーの変化の都度呼ばれる関数)ですが、
1 2 3 4 5 6 7 8 9 | # スクロールバーが変化したときに呼ばれるコールバック関数 def scrl_callback(val): tmp = convert_preview(img) w = WIDTH / / 2 h = HEIGHT / / 2 image_rgb = cv2.cvtColor(cv2.resize(tmp,(w,h)),cv2.COLOR_BGR2RGB) global image_Tk image_Tk = ImageTk.PhotoImage(Image.fromarray(image_rgb),master = canvas) canvas.create_image( 0 , 0 ,image = image_Tk,anchor = 'nw' ) |
こんな感じ。指定されたパラメータで画像(img)を変換し、その変換後の画像をtkの画像フォーマットに変換した後に、canvasに適用(create_image)しています。OpenCVで扱う画像からの変換だと煩わしいことに、RGBに直して、PILのフォーマットに直して、Tkのフォーマットにするという3回変換が必要になっています。やむを得ません。
最後に
1 2 | canvas.create_line(w / 2 , 0 ,w / 2 ,h) canvas.create_line( 0 ,h / 2 ,w,h / 2 ) |
ついでに2本のラインを引いて(create_line)、画像の中心が分かるようにしています。なくてもいいですが、画像中心と水平はわかった方が便利なので。
ここで呼ばれるconvert_preview()ですが、これは以前「Pythonでタイムラプス」で作った関数convert()の基本コピペです。同じ関数に渡すパラメータとして共通にしておいて、そのパラメータをそのまま適用できるようにするためです。内部的にはバーの値をダイレクトに読みに行ってたり、画像の補間をより高速なものにしてますが。
完成したGUIは以下の感じ。

まとめ
とりあえずGUIを使って画像切り出しの座標(x,y)、角度、倍率、の4つのパラメータを決定することをサポートするプログラムを作ることができました。ちょっと仕組みを把握しかねている部分はありますが、ひとまず狙い通りに動いてくれてます。このGUIを使って、以前作ったタイムラプス作成プログラムに渡すパラメータが調整できそうです。
今回UIを設定する部分は最低限のことしかしていません。もっといろいろなオプションが用意されているようです。またRGB画像を前提としています。グレースケール画像はダメかな。(cv2.imreadで逃げられるのかな)
コードも直感的です。特段トリッキーなコードを書くわけでもなく、60行程度で実現できました。別に行数が少ないことが正義というわけではないですが、少なければ少ないだけ全体を見通すことができて応用が利きます。
凝った事をしだせばきりがないのですが、なんかPythonで凝ったUIを作るのは本質じゃない気がしたので、いったんは極シンプルなUIを作ってみました。画像を見ながらその調整値(パラメータ、閾値)を決めるのは、みんなやりたくなるかなと思うので、必要な機能なり知識かなとは思いますが、いったんはこのレベルで抑えときます。UI実装はClassを使っているサンプルが多いですが、今回はベタに書きました。機能を把握するにはいったんこっちの方がよさそうだったので。
とはいえ画像1枚からでは少しパラメータが決めにくいです。もう少し機能追加します。
コメント