PythonでタイムラプスとGUI その5(class 2)

python

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

python現像
スポンサーリンク
キャンプ工学

コメント

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