PythonでタイムラプスとGUI その1(tkinter)

python

 初心者のための目的志向プログラミング。以前の記事でタイムラプス動画を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は以下の感じ。

Image

まとめ

 とりあえずGUIを使って画像切り出しの座標(x,y)、角度、倍率、の4つのパラメータを決定することをサポートするプログラムを作ることができました。ちょっと仕組みを把握しかねている部分はありますが、ひとまず狙い通りに動いてくれてます。このGUIを使って、以前作ったタイムラプス作成プログラムに渡すパラメータが調整できそうです。

 今回UIを設定する部分は最低限のことしかしていません。もっといろいろなオプションが用意されているようです。またRGB画像を前提としています。グレースケール画像はダメかな。(cv2.imreadで逃げられるのかな)

 コードも直感的です。特段トリッキーなコードを書くわけでもなく、60行程度で実現できました。別に行数が少ないことが正義というわけではないですが、少なければ少ないだけ全体を見通すことができて応用が利きます。

 凝った事をしだせばきりがないのですが、なんかPythonで凝ったUIを作るのは本質じゃない気がしたので、いったんは極シンプルなUIを作ってみました。画像を見ながらその調整値(パラメータ、閾値)を決めるのは、みんなやりたくなるかなと思うので、必要な機能なり知識かなとは思いますが、いったんはこのレベルで抑えときます。UI実装はClassを使っているサンプルが多いですが、今回はベタに書きました。機能を把握するにはいったんこっちの方がよさそうだったので。

 とはいえ画像1枚からでは少しパラメータが決めにくいです。もう少し機能追加します。

python画像処理
スポンサーリンク
キャンプ工学

コメント

タイトルとURLをコピーしました