初心者のための目的志向プログラミング。以前の記事でタイムラプス動画をPython使って、連番JPEGから作る方法をステップを追って書いてみました。ただこのプログラムですが、完全にスクリプトの状態で、各種パラメータはプログラムベタ書きの、CUIベースのプログラムになっています。
そこで、今回も勉強がてらPythonでGUIを作って、画角の調整をするプログラムを何回かに分けて、作ってみようと思います。
tkinterの導入
簡単なGUIを作るライブラリとしてtkinterというライブラリがPythonには用意されています。以前の記事で画像表示の1手段としてのこれを書いたことがありますが、これを発展させて、画角の調整用バーを加え、プログラムベタ書きだったパラメータを調整しやすくする仕組みを考えます。
やりたいこと
以前のスクリプトであらかじめベタで指定することになっていた、画像切り出しの座標(x,y)、角度、倍率、の4つのパラメータを、GUIによる操作で取得する。
切り出し後の出力画像を確認しながら操作する。
方針
tkinterのCanvasに画像を表示しながら、4本のScale(バー)を操作し、その操作結果をパラメータとしてダイナミックに画像の切り出し処理に反映させ、パラメータを決定する。
スクロール変化のコールバックを受けて、以前書いたタイムラプス作成プログラムで作った関数をコールし、画像の変換を行う。
コード
はじめの一歩的には、以前作成した画像表示プログラム
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本追加して、その変化のコールバックを受ける関数を作ってみただけです。
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とかにした方がかっこいいかもしれません。
まず先頭の
WIDTH = 1920 HEIGHT = 1080 FILE = 'Photo_0001.jpg'
は定数。切り出し後の画像サイズと扱う画像ファイルを指定しています。
処理の本流は以下から開始。
root = tk.Tk() img = cv2.imread(FILE)
でTkのオブジェクトを作って、置いときます。半分お約束です。同様に画像も読んで置いときます。画像変換の関数がOpenCVなので、開いて置いておくのもOpenCVを使います。
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スケール) 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をしてしているのかよくわかっていません。リファレンスを見てもよくわからなかった。
# 下側(バーがゴリゴリある方) fm_ctrl = tk.Frame(pw_main) pw_main.add(fm_ctrl)
同様にFrameをレイアウトします。このFrameに今度はScaleウィジェットをレイアウトします。
# フレームにバーを配置 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をセットしています。
続いてコールバックされる関数(バーの変化の都度呼ばれる関数)ですが、
# スクロールバーが変化したときに呼ばれるコールバック関数 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回変換が必要になっています。やむを得ません。
最後に
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枚からでは少しパラメータが決めにくいです。もう少し機能追加します。
コメント