初心者のための目的志向プログラミング。今回も勉強がてらPythonでGUIを作って、撮影後の画像の画角調整をするプログラムの続きを、作ってみようと思います。
今回はメニューの追加。コモンダイアログ的な機能の追加を試してみます。
前回までである1枚の画像に対して、パラメータを調整するUIができました。今回は複数の画像でそれが簡単にできる仕組みを考えます。
タイトルのタイムラプスがだんだん関係なくなってますが…。
やりたいこと
以前のスクリプトであらかじめ指定することになっていた、画像切り出しの座標(x,y)、角度、倍率、の4つのパラメータを、GUIによる操作で取得する。切り出し後の出力画像を確認しながら操作する。
ここまで前回完成
特定のフォルダにある画像列を任意に選んで、画角の確認ができるようにする。最終出力動画がイメージしやすいようにする。
方針
メニューを追加し、画像ファイルが置いてあるフォルダを、いつもの感覚で選択できるようにする。
バー(フレーム選択)をもう一つ追加し、そのバーを動かすことで、フォルダ内の任意のJPEGファイルを選べるようにする。
コード
import tkinter as tk
import tkinter.filedialog
import cv2
from PIL import ImageTk, Image
import glob
import numpy as np
WIDTH = 1920
HEIGHT = 1080
# スクロールバーが変化したときに呼ばれるコールバック関数
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')
canvas.create_line(w/2,0,w/2,h)
canvas.create_line(0,h/2,w,h/2)
# こいつが回転シフト拡大を行う処理本体
def convert_preview(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)
# フォルダを開くアクション
def open_dir():
fileName = tk.filedialog.askdirectory()
global Var_Flist
global flist
flist = glob.glob(fileName+"\\*.jpg")
Var_flist["to"] = len(flist)
img_update(1)
# 画像ファイルのアップデート(フレーム変更時のコールバック関数)
def img_update(val):
if len(flist) > 0:
global img
img = cv2.imread(flist[int(val)-1])
root.title(flist[int(val)-1]) #タイトルにファイル名表示
scrl_callback(0)
root = tk.Tk()
flist = []
img = np.zeros((32,32,3), np.uint8)
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_shifty = tk.Scale(fm_ctrl, label='縦方向シフト量', orient="v", from_=-1000, to=1000, command=scrl_callback)
Var_shiftx = tk.Scale(fm_ctrl, label='横方向シフト量', orient="h", from_=-1000, to=1000, command=scrl_callback)
Var_flist = tk.Scale(fm_ctrl, label='フレーム', orient="h", from_=1, to=len(flist), command=img_update)
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)
Var_flist.pack(fill=tk.X)
Var_scale.set(1.0)
#menu bar
men = tk.Menu(root)
menu_file = tk.Menu(root, tearoff=0)
men.add_cascade(label='ファイル',menu=menu_file)
menu_file.add_command(label='フォルダを開く',command=open_dir)
root.config(menu=men)
scrl_callback(0)
root.mainloop()
解説
前回のコードに対して、main部の変更に加え、2つ関数が増えています。画像があるフォルダを開くための関数と、画像ファイルをアップデートする関数です。以前の記事で記載した説明は割愛します。
main部
まずmainに相当する部分で、まず空のファイルリストと、真っ黒画像を用意するコードを追加しています。
flist = [] #空リスト
img = np.zeros((32,32,3), np.uint8) #真っ黒画像
最初に表示する画像が必要だったので、今回は32×32の真っ黒画像を用意しました。なので最初はcanvasに真っ黒が表示されています。
このflistに画像ファイル名のリストを保持します。最初は空。
続いて、新たにScaleウィジェットのVar_flistを作っています。
Var_flist = tk.Scale(fm_ctrl, label='フレーム', orient="h", from_=1, to=len(flist), command=img_update)
作り方は同じ。向きは横(orient=”h”)にしました。また最大値をファイルリストのサイズにしています。なので、最初は1始まりの0終わりという少し変なことになってます。が問題なのでそのまま行きます。バグではありません。仕様です。
ここで呼ばれるコールバック関数( img_update() )は新規作成です。後述します。
最後にフォルダを開くためのメニューを作ります。
#menu bar
men = tk.Menu(root) #メインメニュー
menu_file = tk.Menu(root, tearoff=0) #子メニュー
men.add_cascade(label='ファイル',menu=menu_file)
menu_file.add_command(label='フォルダを開く',command=open_dir)
root.config(menu=men)
menがルートのメニューで、これに子メニュー(プルダウンメニュー)を追加していくようです。同じtk.Menuクラスでよいようです。
ここで子メニューを作る際に、tearoff=0にしないと、メニューに切り取り線が表示され、分離可能なメニューができてしまいます。なぜこれがデフォルトでないのか、少し不思議です。
今回は子メニューとして「ファイル」。これしか作らないのでこれだけです。そしてそのそのファイルメニューの下に「フォルダを開く」コマンドをレイアウトします。そのフォルダを開くが選ばれた際にコールバックされる関数を指定するようです。
今回の様に単機能であれば、プルダウンメニューを用意する必要はないのかもしれませんが、いったんもっともらしいものにするため、こうします。
ここで呼ばれるコールバック関数も新規です。後述します。
コールバック関数 その1 open_dir()
この関数では、いわゆるcommon dialogを使って、一般的なUIからディレクトリの選択を行います。tkinterにはそれも用意されています。そして開いたフォルダ内にあるJPEGのリストを取得し、フレームとして新規作成したバーでアクセスできるようにします。
import tkinter.filedialog
をインポートしたうえで、
fileName = tk.filedialog.askdirectory()
にてフォルダ名を取得できます。そのうえで、得られたフォルダにあるJPEGファイルのリストを作ります。
flist = glob.glob(fileName+"\\*.jpg")
glob()で、指定されたフォルダ以下の*jpgのファイル名のリストが取れます。便利です。この関数を使うために、
import glob
してます。
そして、先に作ったVar_flistの最終フレーム番号を更新します。今回取得されたファイルの数だけフレーム数を設定します。つまりバーの値がそのままリストのインデックスになるようにします。
Var_flist["to"] = len(flist)
このようにして、リストを更新し、最後表示画像のアップデートをします。この配列にラベルがついているスタイルもイマイチしっくりこないのですが、慣れることにします。ライブラリをたたきまくっているだけですね。
コールバック関数 その2 img_update()
表示画像ファイルのアップデートをします。引数で指定されたインデックスを使って、ファイルリストからファイル名を取得し、その取得されたファイル名の画像を開く動作をします。一応リストが空かどうかを確認してから動作するようにします。
あとついでに、ウィンドウのタイトルに現在開いている画像ファイルの名前を表示するようにしました。なんのファイルを開けているかわかるようになりました。
まとめ
これで、バーをぐりぐりするだけで、フレームをまたいで画像を表示することができるようになり、最初から最後のフレームまで、どのようにパン、ズームするか、最終出力がイメージしやすくなりました。
ファイルも固定じゃなくなったので、任意のファイルが選択しやすくなりました。Windowsで動きを確認してますが、OS依存は無い認識。
画像の変換部で2度の変換(実際の処理に即した変換と、その表示のための縮小)しているので、そろそろ速度が気になりますが、無視します。
PythonでGUIコーディング。なんかPythonの本質からどんどん離れていくような気がしてきますが、簡単だし面白いので続けます。
GUIの記述部にまだ一部理解が追い付いていませんが、シンプルで直感的です。これで90行。だんだん長くなってきましたが、まだ楽にメンテできる範囲。機能追加する気になるので、今度はこのGUIを使って、最終動画まで作ってしまうことにします。
コメント