Pythonのコードの勉強がてら、趣味のタイムラプス動画を自前で作るコードを色々書いてきました。以前のその4で完成したつもりになっていましたが、色々コードを追記したので、せっかくなのでそのコードだけでも載せておこうと思います。
タイムラプス処理本体
これは以前記述したコードを丸まんま使います。
機能としては最低限、
・画像のリスト
・切り出しスタート位置(x,y座標、サイズ、角度)
・切り出し終了位置
を指定したうえで、その間をパン、ズームしながら動画化するスクリプトになっています。フェードイン/アウトのフレーム数や、動画のフレームレートはこちらのコードに定数としてベタ書きです。これもGUIから操作できる変数にすればよかったかも。
このコードをmake_laps.pyとかそんな名前で保存しておき、今回のプログラムと同一フォルダに置いておきます。
GUI部
今回のメインはこれです。以前GUIは
こんな感じで作っていったん満足していたのですが、その後まじめに画像ビューアーを作りました。
この機能をマージしない手はないなと思い、今回はそれをマージした大作になっています。
操作部のイメージとしてはこんな感じ。

メインの画像表示部は、ほぼ前述のViewerを作る。で記述したまんま。あとは複数画像をスクロールするためのバー(フレーム)。これもマウスホイールで動くように細工しました。あと開始終了フレームの設定を保存するボタン。最後に開始終了フレームを表示しておく小窓を2つ。小窓はクリックするとそのフレームにジャンプする小技付き。
あと処理中にGUIが固まるのがダサいので、threadとして変換プロセスを走らせるようにしました。ここで調べたコードを流用しています。
見た目はあまりモダンな感じではないですが、これだけごてごてしたGUIが約300行のコードで書けてしまいます。すげーなPython。ここまで作っておけば、数年後の自分でも簡単にタイムラプス動画が作れそうです。
コード
そしてコードです。これだけ長いとコードはGitHubとかに置くんですかね?一般的には。とはいえ自分ひとりで書いてるし、バージョン管理するほどでもないし。ベタに張り付けておきます。
import tkinter as tk
import tkinter.filedialog
import cv2
from PIL import ImageTk, Image
import glob
import numpy as np
import make_laps #これがタイムラプスを作る本体
import threading
# make_laps側と幅・高は同期させる
WIDTH = make_laps.WIDTH
HEIGHT = make_laps.HEIGHT
# 表示用GUIクラス
class laps_viewerGUI(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.img = np.zeros((32, 32, 3), np.uint8) # 初期表示用画像
self.flist = [] # ファイルリスト
self.settings = [np.array([0,0,0,1.0]),np.array([0,0,0,1.0])] # 開始終了の設定
self.framenum = [0,0] # 開始終了のフレーム番号
self.processing = False
# ----------------------# 上側のフレーム(画像表示部)#----------------------#
fm_upper = tk.Frame(master)
fm_upper.pack(fill=tk.X, side=tk.TOP)
# 表示は実画像サイズの半分で行う
self.canvas = tk.Canvas(fm_upper, width=WIDTH//2, height=HEIGHT//2)
self.canvas.bind("<Button-1>", self.click)
self.canvas.bind("<B1-Motion>", self.drag)
self.canvas.bind("<Control-MouseWheel>", self.ctrl_wheel)
self.canvas.bind("<MouseWheel>", self.wheel)
self.canvas.pack(side=tk.LEFT)
self.image_Tk = ImageTk.PhotoImage(Image.fromarray(self.img), master=self.canvas) # 表示画像の生成
# 縦スクロールバー
self.Var_shifty = tk.Scale(fm_upper, showvalue=False, orient="v", command=self.scrl_callback)
self.Var_shifty.pack(fill=tk.Y, side=tk.RIGHT)
# ----------------------# 真ん中のフレーム # ---------------------- #
fm_mid = tk.Frame(master)
fm_mid.pack(fill=tk.X, side=tk.TOP)
# 横スクロールバー
self.Var_shiftx = tk.Scale(fm_mid, showvalue=False, orient="h", command=self.scrl_callback)
self.Var_shiftx.pack(fill=tk.X)
# グリッド表示・非表示
self.Chkbox = tk.BooleanVar()
self.Chkbox.set(True)
chkbtn = tk.Checkbutton(fm_mid, variable=self.Chkbox, text='グリッドを表示する', command=self.redraw)
chkbtn.pack(side=tk.RIGHT)
# 情報表示部
self.label = tk.Label(fm_mid, text="Info")
self.label.pack(side=tk.LEFT)
# ----------------------# 下側のフレーム #----------------------#
fm_ctrl = tk.Frame(master)
fm_ctrl.pack(fill=tk.X, side=tk.TOP)
# 拡大縮小・回転
self.Var_scale = tk.Scale(fm_ctrl, label='倍率', orient="v", from_=0.1, to=16.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_flist = tk.Scale(fm_ctrl, label='フレーム', orient="h", from_=1, to=len(self.flist), command=self.img_update)
self.Var_angle.pack(fill=tk.Y, side=tk.LEFT)
self.Var_scale.pack(fill=tk.Y, side=tk.LEFT)
self.Var_flist.pack(fill=tk.X)
self.Var_scale.set(1.0)
# 開始/終了フレーム決定ボタン
button1 = tk.Button(fm_ctrl, text="開始フレーム", command=self.button1_clicked, width=15)
button2 = tk.Button(fm_ctrl, text="終了フレーム", command=self.button2_clicked, width=15)
button1.pack(side=tk.LEFT)
button2.pack(side=tk.LEFT)
# Start/End frame 表示用小窓(256x144固定 16:9の動画を想定)
self.Cnvs_frame = []
self.Cnvs_frame.append(tk.Canvas(fm_ctrl, width=256, height=144))
self.Cnvs_frame.append(tk.Canvas(fm_ctrl, width=256, height=144))
self.Cnvs_frame[0].bind("<Button-1>", self.canvas0_clicked)
self.Cnvs_frame[1].bind("<Button-1>", self.canvas1_clicked)
self.Cnvs_frame[1].pack(side=tk.RIGHT)
self.Cnvs_frame[0].pack(side=tk.RIGHT)
self.frame_Tk = []
self.frame_Tk.append(ImageTk.PhotoImage(Image.fromarray(self.img), master=self.Cnvs_frame[0]))
self.frame_Tk.append(ImageTk.PhotoImage(Image.fromarray(self.img), master=self.Cnvs_frame[1]))
# ----------------------# メニュー #----------------------#
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.makelaps)
master.config(menu=men)
# メインキャンパスでのマウスダウン
def click(self, event):
self.posx = event.x
self.posy = event.y
# メインキャンパスでのマウスムーブ
def drag(self, event):
dx = event.x - self.posx
dy = event.y - self.posy
self.Var_shiftx.set(self.Var_shiftx.get() + dx)
self.Var_shifty.set(self.Var_shifty.get() + dy)
self.click(event)
# メインキャンパスでのマウスホイール(拡大縮小)
def ctrl_wheel(self, event):
if event.delta > 0: # 向きのみ検出
self.Var_scale.set(self.Var_scale.get()*2)
else:
self.Var_scale.set(self.Var_scale.get()/2)
# メインキャンパスでのマウスホイール(フレーム移動)
def wheel(self, event):
if event.delta > 0: # 向きのみ検出
self.Var_flist.set(self.Var_flist.get()-1)
else:
self.Var_flist.set(self.Var_flist.get()+1)
# 画像再表示
def redraw(self):
self.scrl_callback(0)
# スクロールバーが変化したときに呼ばれるコールバック関数
def scrl_callback(self, val):
self.calc_scrl_limit()
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')
# グリッドの表示
if self.Chkbox.get():
self.canvas.create_line(w / 2, 0, w / 2, h, fill='red')
self.canvas.create_line(0, h / 2, w, h / 2, fill='red')
# 画像ファイルのアップデート(フレーム変更時のコールバック関数)
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.redraw()
# 倍率変化に応じた スクロール限界の設定
def calc_scrl_limit(self):
h, w = self.img.shape[:2]
fm = (WIDTH - w*self.Var_scale.get())/2
if fm < 0:
fm = -fm
self.Var_shiftx["from"] = -fm
self.Var_shiftx["to"] = fm
fm = (HEIGHT - h*self.Var_scale.get())/2
if fm < 0:
fm = -fm
self.Var_shifty["from"] = -fm
self.Var_shifty["to"] = fm
# 回転シフト拡大を行う処理本体(画像処理らしい処理はここだけ)
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() + (WIDTH - w) // 2
affine[1, 2] += self.Var_shifty.get() + (HEIGHT - h) // 2
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 convert(self):
if len(self.flist) > 0: # リストが空でなければ保存
fileName = tk.filedialog.asksaveasfilename(filetypes = [("movie file", ".mp4")])
if fileName != "": # キャンセルが押されると名前が空のようだ
if not fileName.endswith('.mp4'): # 拡張子の有無を確認
fileName = fileName + '.mp4'
sfrm = self.framenum[0] - 1
efrm = self.framenum[1]
step = 1
if efrm < sfrm: # スタートとエンドの大小関係が逆なら、逆回し
efrm -= 1
step = -1
filelist = self.flist[sfrm:efrm:step]
h, w = self.img.shape[:2]
spt = self.settings[0]
ept = self.settings[1]
spt[0] = -(spt[0] + (WIDTH - w) // 2)
ept[0] = -(ept[0] + (WIDTH - w) // 2)
spt[1] = -(spt[1] + (HEIGHT - h) // 2)
ept[1] = -(ept[1] + (HEIGHT - h) // 2)
make_laps.makeLaps(filelist,fileName,spt,ept) #変換開始
self.label.config(text="処理終了")
self.processing = False
# menu 保存 変換開始
def makelaps(self):
if self.processing: # 処理中は押せない
return
self.processing = True
self.label.config(text="処理中.....")
thread = threading.Thread(target=self.convert)
thread.start()
# 開始終了パラメータ設定および表示
def set_params(self, i):
# パラメータの設定
self.framenum[i] = self.Var_flist.get()
self.settings[i] = np.array([self.Var_shiftx.get(),self.Var_shifty.get(),self.Var_angle.get(),self.Var_scale.get()])
# 小窓表示
tmp = self.convert_preview(self.img)
image_rgb = cv2.cvtColor(cv2.resize(tmp, (256, 144)), cv2.COLOR_BGR2RGB)
self.frame_Tk[i] = ImageTk.PhotoImage(Image.fromarray(image_rgb), master=self.Cnvs_frame[i])
self.Cnvs_frame[i].create_image(0, 0, image=self.frame_Tk[i], anchor='nw')
if self.processing: # 処理中は情報表示は更新させない。(処理ステータス表示用)
return
# 開始終了フレーム情報表示
info = "start frame: " + str(self.framenum[0]) + " / end frame: " + str(self.framenum[1])
info += " / total " + str(abs(self.framenum[1]-self.framenum[0])+1) + " frames"
if self.framenum[1] < self.framenum[0]:
info += " / inverse"
self.label.config(text=info)
# 開始フレーム選択ボタン
def button1_clicked(self):
self.set_params(0)
# 終了フレーム選択ボタン
def button2_clicked(self):
self.set_params(1)
# 設定に基づいて再表示
def redraw_setting(self, i):
self.Var_flist.set(self.framenum[i])
self.Var_scale.set(self.settings[i][3])
self.calc_scrl_limit() # 倍率変更後にスクロール限界の設定
self.Var_shiftx.set(self.settings[i][0])
self.Var_shifty.set(self.settings[i][1])
self.Var_angle.set(self.settings[i][2])
# 小窓のクリック 開始フレームの表示呼び出し
def canvas0_clicked(self, event):
self.redraw_setting(0)
# 小窓のクリック 終了フレームの表示呼び出し
def canvas1_clicked(self, event):
self.redraw_setting(1)
# main
if __name__ == '__main__':
laps_viewerGUI(master=tk.Tk()).mainloop()
眺める気にはならないですよね?コピペして、自分のコード環境で動かしながら見てもらった方が良いと思います。
まとめ
まとめるほどのものはないですね。せっかく書いたので、自分用に乗せてるようなものです。覚書です。ここまでごてごて作っておけば操作忘れないだろうし、300行程度であればメンテもできるかな。
コメント