見出し画像

【PythonでGUIを作りたい】Tkinterで作る画像編集ソフト#10:番外編(TkinterとOpenCVの使い方)

こんにちは。すうちです。

先日Tkinterで作る画像編集ソフトの連載を終えましたが、番外編として画像編集ソフトに用いたTkinterとOpenCVの使い方をまとめたいと思います。



◆はじめに

静止画/動画編集ソフトのコード

ちなみに、前回の記事(最終回)はこちらです。

以下、全体のソースコードです。note公開用にgithubアカウント作りました。

GUI Image Editor(コード一式)

本コードは、Tkinterの学習用に作成したものです。
基本的な動作は確認してますが、テストが十分できてない所もあります。その点を考慮頂き、ご興味ある方は参考にして頂ければと思います。

以降は、上記からTkinterの部品に特化したサンプルです。


◆Tkinter部品(Widget)

今回画像編集ソフトではGUI画面の部品生成と配置、ユーザ操作のイベント処理にTkinterを使いました(ほとんどがViewGUI.pyの部分です)。

メインウィンドウ(root)

import sys
import tkinter as tk
from tkinter import ttk, filedialog

import cv2
from PIL import Image, ImageTk, ImageOps
import os

# Tk MainWindow 生成
root_window = tk.Tk()
# メインウィンドウサイズ指定
root_window.geometry("800x600")
# メインウィンドウタイトル
root_window.title('Tkinter GUI')

# 他の部品設定や配置、イベント登録

# ループ処理
root_window.mainloop()

最初にメインウインドウ(root)を作成します。
geometry()の引数がウィンドウサイズ(”横x縦”)です。 このウィンドウを起点にサブフレームやGUI部品を配置します。最後のmainloop()を実行するとGUIが起動します。

ここから先は、上記コメントの”他の部品設定や配置、イベント登録”にあたる部分です。


サブウィンドウ(Frame)

sub_window = tk.Frame(root_window, height=590, width=540)
sub_window2 = tk.Frame(root_window, height=500, width=300)

最初の引数は配置するフレームです。例はroot_windowを指定してます。heightwidthはフレームのサイズです。


タブ(Notebook)

notebook = ttk.Notebook(sub_window)
tab1 = tk.Frame(notebook, height=560, width=500)
tab2 = tk.Frame(notebook, height=560, width=500)
notebook.add(tab1, text='[Photo]')
notebook.add(tab2, text='[Video]')
notebook.bind('<<NotebookTabChanged>>', event_tabchanged)
notebook.select(tab2)

Notebook()は、tkinterのttkをインポート必要です。引数はFrameと同じくタブを配置したいフレームです。
生成後、notebookに切替え対象のフレームを登録(add)します。

notebook.bind('<<NotebookTabChanged>>', イベント処理)は、タブ切替時のイベント(関数)を設定できます。notebook.select()はタブ選択で例はtab2表示です。


キャンバス(Canvas)

window_canvas = tk.Canvas(tab1, height=450, width=400, bg='gray')
window_canvas2 = tk.Canvas(tab2, height=450, width=400, bg='light blue')

キャンバスはTkinterの画像表示領域です。引数はフレームと基本同じ。bg(background)は背景色の意味です。テキストまたは値で指定できます。


ボタン(Button)

button_0 = tk.Button(sub_window2, text='button_0', width=10, command=event_button)
button_1 = tk.Button(sub_window2, text='button_1', width=10, command=event_button_view2save) 

最初の引数は配置するフレーム、textはボタン表示のテキストです。widthはボタンの幅、commandはボタン押下時に実行するイベントです。

今回使いませんでしたが引数imagetk.PhotoImage()で変換した画像を指定すればアイコンのような表示もできます。


ラベル(Label)

label_0 = tk.Label(sub_window2, text='label_0', width=10)

引数は配置フレーム、テキスト、幅の指定など基本同じです。ラベルのテキストを後から変えたい場合は、ラベル名['text']=xxxとするか後述のEntry例のようにtextvariableStringVar指定して必要に応じて書換えます。


エントリ(Entry)

entry_0 = tk.Entry(sub_window2, text='entry_0', textvariable=str_entry, width=40)
str_entry.set('entry_text')

Entryは1行のテキスト領域です。最初の引数は配置フレームです。後でテキストを変えたいのでtextvariableStringVar変数(後述)を指定します。widthはテキストの幅です。

画像編集ソフトでは選択ディレクトリの表示に使いました。


選択リスト(Combobox)

select_list = ['A','B''C']
combo_0 = ttk.Combobox(sub_window2, text='combo_0'value=select_list, state='readonly', width=30, postcommand=event_combobox)
combo_0.set(select_list[0])
combo_0.bind('<<ComboboxSelected>>', event_combobox_select)

Combobox()はtkinterのttkをインポート必要です。引数は配置フレーム、valueは選択対象のリスト、widthは幅の指定です。state='readonly'は書換不可、postcommandはCombobox操作時のイベント(関数)です。

bind('<<ComboboxSelected>>', イベント名)は、テキスト選択時に実行するイベントを指定できます。

画像編集ソフトでは、選択ファイルを確定した時に使用しました。


スライダ(Scale)

bar_scale = tk.Scale(tab2, from_=0, to_=100, orient='horizontal', resolution=1.0,
                           variable=val_bar, length=400, command=event_scale)
val_bar.set(0)

最初の引数は他の部品と同じ配置フレームです。from_to_は範囲指定、lengthは幅、commandはスライダ操作時の実行イベントです。
orienthorizontalは水平方向(縦方向はvertical指定)を意味し、resolutionはスライダの目盛(刻み幅)です。

紹介した部品はいずれもよく見かけると思いますが、Tkinterは他の部品もあります。

また今回の引数は必要最低限で各部品ごとに他の引数(フォント指定、表示調整やスタイル変更など)もあります。その辺は実際作りたいものや必要に応じて調べてみると面白いかもしれません。


◆GUI部品の配置

place(位置指定)

sub_window.place(relx=0.01, rely=0.01)
sub_window2.place(relx=0.68, rely=0.30)
notebook.place(relx=0.001, rely=0.001)
window_canvas.place(relx=0.09, rely=0.05)
window_canvas2.place(relx=0.09, rely=0.05)
bar_scale.place(relx=0.08, rely=0.85)

grid(行列指定)

button_0.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
entry_0.grid(row=2, column=1, padx=5, pady=5, sticky=tk.W)
label_0.grid(row=3, column=1, padx=5, pady=5, sticky=tk.W)
combo_0.grid(row=4, column=1, padx=5, pady=5, sticky=tk.W)
button_1.grid(row=5, column=1, padx=5, pady=5, sticky=tk.W)

place()grid()は部品配置の指定方法です。

place()は座標指定(任意の位置に配置)です。relxrelyは座標ではなく相対位置で指定する方法(フレーム縦横の何%の位置に配置するか)です。

grid()は(行、列)で配置する方法です。stickyは上下左右(tk.E, tk.W, tk.S, tk.Nは東西南北の頭文字)に引き延ばす調整です。

他にpack()という自動配置もあります。部品が少ない場合や細かい位置指定が不要な時はこちらが便利です。


◆Tkinter関連(その他)

StringVar、IntVar

str_entry = tk.StringVar()
val_bar = tk.IntVar()

文字列や値の変数(インスタンス)です。tkiniter実行中の値を変更したり保持する場合に必要です。各部品の引数textableStringVar(文字列)、variableIntVar(値)の変数を指定できます。


ディレクトリ選択(filedialog)

dir_path = filedialog.askdirectory(initialdir='C:', mustexist=True)
str_entry.set(dir_path)

filedialogはディレクトリ選択のUIを実現できます。initialdirは起動時のディレクトリパス、mustexistは存在するディレクトリのみ選択可の意味です。

また、filedialog.askopenfilename()でファイルパスの取得もできます。

bind

window_canvas.bind('<ButtonPress-1>', event_motion_start)
window_canvas.bind('<Button1-Motion>', event_motion_keep)
window_canvas.bind('<ButtonRelease-1>', event_motion_end)
window_canvas2.bind('<Double-Button-1>', event_double_clik)

前述で説明したbindは部品に特定操作した時のイベント登録に使います。画像編集ソフトではマウスイベントしか取ってませんが他にキー操作も判定できます。


イベント処理(サンプル)

前述の部品で指定するイベント処理のサンプルです(イベント実行確認用)

def event_tabchanged(self):
    print('tab changed')

def event_motion_start(self):
    print(sys._getframe().f_code.co_name)
    
def event_motion_keep(self):
    print(sys._getframe().f_code.co_name)
    
def event_motion_end(self):
    print(sys._getframe().f_code.co_name)
    
def event_double_clik(self):
    print(sys._getframe().f_code.co_name)


def event_button():
    print(sys._getframe().f_code.co_name)
    dir_path = filedialog.askdirectory(initialdir='C:', mustexist=True)
    str_entry.set(dir_path)

def event_combobox():
    print(sys._getframe().f_code.co_name)
    
def event_combobox_select(self):
    print(sys._getframe().f_code.co_name)

def event_scale(val):
    print(sys._getframe().f_code.co_name)
    print('scale-val:', val)


◆TkinterとOpenCVの連携

OpenCVは動画の画像表示と保存に使いました。

動画表示&保存

以下、button_1を押した時のイベント処理サンプル(画像編集ソフトのコードを簡略化したもの)です。

def event_button_view2save():
    print(sys._getframe().f_code.co_name)
    
    global tk_video
    
    # ビデオ画像表示
    file_path = '表示したいmp4ファイルのパス'
    cap = cv2.VideoCapture(file_path)
    frame_num = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = int(cap.get(cv2.CAP_PROP_FPS))
    
    print('total frames:{}, fps:{}'.format(frame_num, fps))
    
    cur_frame = 0
    cap.set(cv2.CAP_PROP_POS_FRAMES, cur_frame)
    
    ret, video_img = cap.read()
    if ret:
        # Canvas画像表示
        canvas_w_video = window_canvas2.winfo_width()
        canvas_h_video = window_canvas2.winfo_height()
        
        img_conv = cv2.cvtColor(video_img, cv2.COLOR_BGR2RGB)             
        img_conv = Image.fromarray(img_conv)
        pil_img = ImageOps.pad(img_conv, (canvas_w_video, canvas_h_video))
        tk_video = ImageTk.PhotoImage(image=pil_img)
    
        window_canvas2.create_image(canvas_w_video/2, canvas_h_video/2, image=tk_video, tag='video')
    
    
        # ビデオ画像保存    
        name, ext = os.path.splitext(file_path)
        file_path = '{}_save.mp4'.format(name)
        
        video_format = cv2.VideoWriter_fourcc(*'mp4v')
        v_height, v_width, RGB_ch = video_img.shape
        save_video = cv2.VideoWriter(file_path, video_format, fps, (v_width, v_height))
        
        cur_frame = 0
        cap.set(cv2.CAP_PROP_POS_FRAMES, cur_frame)
        save_frames = 60
        
        for n in range(save_frames):
            ret, frame = cap.read()
            if ret:
                save_video.write(frame)
                
                if n % 10 == 0:
                    print('{}/{}'.format(n, frame_num))
                
        save_video.release()
        print("Saved: {}".format(file_path))
    
    cap.release()

動画表示
最初にcv2.VideoCapture(’ファイルパス指定’)で表示する動画ファイルのインスタンスを取得します。

cap.get()の引数cv2.CAP_PROP_FRAME_COUNTは動画の総フレーム数、cv2.CAP_PROP_FPSは動画のフレームレートを取得できます。

cap.set(cv2.CAP_PROP_POS_FRAMES, フレーム番号)はフレーム位置の設定です。この例は0に設定してcap.read()で対象フレームを読み出しています。

注意点は、tk形式画像の保存先(tk_video)のglobal宣言です。これをやらないとGUIに表示されません(※画像編集ソフトはクラスのインスタンス内に保存、イベント処理内の変数は処理を抜けた後に破棄されるため)。

tkinterはOpenCVで読んだ画像形式をそのままでは表示できません。

cv2.cvtColor()からImageTk.PhotoImage()まではOpenCV形式からTkinter形式への画像変換です(※ImageOps.pad()はCanvasサイズにあわせるためのサイズ調整)。

キャンバス表示は、’Canvas名.create_image()でtkinter上に表示します。

動画保存
動画保存は、cv2.VideoWriter_fourcc()cv2.VideoWriter()にて保存する動画フォーマット、ファイルパス、フレームレート、幅・高さを設定後、保存対象のフレームを読み出して、'動画保存のインスタンス'.write()でフレームを書き込み動画ファイルを作成します。


最後に

先日作成した画像編集ソフトは、tkiniterを使うための本質と少し離れた所もあったので、本記事を書くことにしました。

今回のサンプルコードを全てコピペして実行すると、必要最低限tkinterの部品が入った骨組みと動作を確認できると思います。

必要に応じて部品や設定を変えたり、イベントを追加することもできるはずなので、もしtkiniterに興味ある方は試してみてください。

GUI構築やTkinterを使うことを検討されている方など、何か参考になれば幸いです。

最後まで読んで頂き、ありがとうございました。

ーーー

ソフトウェアを作る過程を体系的に学びたい方向けの本を書きました(連載記事に大幅に加筆・修正)

Kindle Unlimited 会員の方は無料で読めます。


#連載記事の一覧

ーーー


この記事が気に入ったらサポートをしてみませんか?