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行程度であればメンテもできるかな。
コメント