PythonのTkinterを使ってポップアップ画面を作る方法::USBカメラで画像撮影する際、条件を一括設定したいときに便利

2020年12月12日

今日は Tkinter を使ってポップアップ画面を作ってみます。

ポップアップ画面で条件を一括設定したいとか、色んなプログラムを切り替えて使いたいとか、そんなニーズにピッタリだと思います。

PySimpleGUIもTkinterもどちらでもGUI画面は作成できますが、ポップアップ画面の作り易さはTkinterの方が上だと思います。

また、Tkinterは動作が軽いのも魅力です。

今回ご紹介するプログラムは簡単に作成できて応用範囲も広いです。

アイデア次第で色々作れるようになると思いますので、参考にしてください。

実行画面

以下に実行画面を示します。

プログラムを実行すると、右上の「メニュー画面」が現れます。

画面の中央に Capture と書かれています。

このラベンダー色(薄紫色)の部分は、全体がボタンになっています。

マウスでクリックすると、その下のポップアップ画面が現れます。

Cam ID ~ Directory の文字の横にドロップダウンリスト(マウスでクリックすると選択候補のリストが現れる)があります。

選択したい内容を、あらかじめソースコードに書いておくと、その内容がリストで出てきて選べるようになっています。

Another Dir の文字の横にはテキストボックスが表示されます

このテキストボックスには、直接文字を入力することができます。

ドロップダウンリストで選択した内容やテキストボックスに記載した内容は、プログラム内で変数として保持されます。

この変数は、importで呼び込んだ別のプログラムに引数として値を渡すことができます。

今回は、Captureプログラム(USBカメラの映像を取り込み、画像として保存するプログラム)をimportして(呼び込んで)、撮影条件を変数として渡しています。

状況に応じて撮影条件を変更したい場合は、今回のようなプログラムの方が便利だと思います。

ポップアップ画面の一番下にグレー色の Capture と書かれたボタンがあります。

ここをマウスでクリックすると、Captureプログラムが呼び出されて左の画面(USBカメラの映像)が現れます。

ソースコード

メニュー画面とポップアップ画面の作成

以下のコードをみてください。

import sys
import tkinter
from tkinter import ttk
from PIL import Image, ImageTk

import capture_gui  # 別プログラムの呼び出し


### tkinterでGUIを作成 ###
class Application(tkinter.Frame):

    def __init__(self, master=None):
        super().__init__(master)
        self.pack()
        self.create_widgets()

    def create_widgets(self):
        # バックグラウンドの色候補
        self.A0 = tkinter.Button(self,
                                 width=35,
                                 height=4,
                                 font=("Times New Roman", 15),
                                 fg="gray35",
                                 bg="lavender"
                                 )
        self.A0["text"] = "Capture"
        self.A0["command"] = self.out_cap
        self.A0.pack(side="top")

    ##### capture section #####
    def out_cap(self):
        filewin = tkinter.Toplevel(root)
        filewin.title("menu")
        filewin.geometry("500x525+800+200")
        canvas = tkinter.Canvas(filewin, width=500, height=380, bg="white")
        canvas.pack()

        ### ラベルの作成 ###
        # id, w x h, grid, dir
        label_camera_id = tkinter.Label(filewin, text="Cam ID", font=("Arial", 18), fg="gray35", bg="white")
        label_cap_wh = tkinter.Label(filewin, text="W x H", font=("Arial", 18), fg="gray35", bg="white")
        label_grid = tkinter.Label(filewin, text="Grid", font=("Arial", 18), fg="gray35", bg="white")
        label_directory = tkinter.Label(filewin, text="Directory", font=("Arial", 18), fg="gray35", bg="white")

        # Directory
        label_text = tkinter.Label(filewin, text="Another Dir", font=("Arial", 18), fg="gray35", bg="white")
    
        ### 位置調整 ###
        xx = 15       # ラベル位置: x方向
        yy = 0        # ラベル位置: y方向
        bx = 10       # セレクトボックスの位置: x方向
        by = 0        # セレクトボックスの位置: y方向
        btnx = 0     # ボタンの位置: x方向
        btny = 0      # ボタンの位置: y方向

        ### 各ラベルの位置 ###
        label_camera_id.place(x=65+xx, y=43+yy)
        label_cap_wh.place(x=80+xx, y=103+yy)
        label_grid.place(x=105+xx, y=163+yy)
        label_directory.place(x=50+xx, y=223+yy)
        label_text.place(x=20+xx, y=280+yy)

        # テキストボックスの大きさ と 位置
        text = tkinter.Text(filewin, bg="beige")
        text.place(x=200+bx, y=290+by, width=250, height=75)

        ### CAPの実行 ###
        def click_btn_cap():
            global cam_id, cap_size, grid_sepa, directory_path
            txt = text.get("1.0", "end-1c")
            print(txt)
            capture_gui.cap_img(txt, cam_id, cap_size, grid_sepa, directory_path)

        ### Cam ID の選択 ###
        cam_id = 0

        def cb_selected_id(event):
            global cam_id

            print('cam_id = %s' % cam1.get())

            if cam1.get() == "L-815":
                print("L-815")
                cam_id = 0
                
            elif cam1.get() == "ELP_5-50mm_2MP":
                print("ELP_5-50mm_2MP")
                cam_id = 1

            elif cam1.get() == "ELP_5-50mm_8MP":
                print("ELP_5-50mm_8MP")
                cam_id = 2

        cam1 = tkinter.StringVar()
        cb = ttk.Combobox(filewin, textvariable = cam1)
        cb.bind('<>', cb_selected_id)

        cb['values'] = ('L-815', 'ELP_5-50mm_2MP', 'ELP_5-50mm_8MP')
        cb.set("select")
        cb.place(x=200+bx, y=51+by)

        ### capture画面サイズの選択 ###
        cap_size = 0

        def cb_selected_size(event):
            global cap_size

            print('c1 = %s' % c1.get())

            if c1.get() == "480 x 480":
                print("1")
                cap_size = 1
                
            elif c1.get() == "720 x 720":
                print("2")
                cap_size = 2

            elif c1.get() == "900 x 900":
                print("3")
                cap_size = 3

        c1 = tkinter.StringVar()
        cb = ttk.Combobox(filewin, textvariable = c1)
        cb.bind('<>', cb_selected_size)

        cb['values'] = ('480 x 480', '720 x 720', '900 x 900')
        cb.set("select")
        cb.place(x=200+bx, y=111+by)

        ### grid間隔の選択 ###
        grid_sepa = 0

        def cb_selected_grid(event):
            global grid_sepa

            print('grid = %s' % g1.get())

            if g1.get() == "0":
                print("0")
                grid_sepa = 0

            elif g1.get() == "25":
                print("25")
                grid_sepa = 25
                
            elif g1.get() == "50":
                print("50")
                grid_sepa = 50

            elif g1.get() == "100":
                print("100")
                grid_sepa = 100

            elif g1.get() == "150":
                print("150")
                grid_sepa = 150

            elif g1.get() == "200":
                print("200")
                grid_sepa = 200

        g1 = tkinter.StringVar()
        cb = ttk.Combobox(filewin, textvariable = g1)
        cb.bind('<>', cb_selected_grid)

        cb['values'] = ('0', '25', '50', '100', '150', '200')
        cb.set("select")
        cb.place(x=200+bx, y=173+by)

        ### directoryの選択 ###
        directory_path = 0

        def cb_selected_dir(event):
            global directory_path

            print('d1 = %s' % d1.get())

            if d1.get() == "default":
                print("1")
                directory_path = 1
                text.insert(tkinter.END, "./cnn_act/capture") # キャプチャ画像の保存先

            elif d1.get() == "another":
                print("2")
                directory_path = 2  # こちらを選択した場合は、テキストボックスに画像の保存先パスを記載

        d1 = tkinter.StringVar()
        cb = ttk.Combobox(filewin, textvariable = d1)
        cb.bind('<>', cb_selected_dir)

        cb['values'] = ('default', 'another')
        cb.set("select")
        cb.place(x=200+bx, y=233+by)
    
        ### Capture ボタン ###
        button_rec = tkinter.Button(filewin, text="Capture", font=("Times New Roman", 22),
                                    width=17, height=2,
                                    fg="white", bg="gray40",
                                    command=click_btn_cap
                                    )
        button_rec.place(x=90+btnx, y=400+btny)

### メニュー画面の設定 ###
root = tkinter.Tk()
root.title('メニュー画面')               # メニュー画面のタイトル
root.attributes("-toolwindow", True)
root.geometry("+1430+80")               # メニュー画面の表示位置

# ツールバーのボタンが選ばれたときに実行する関数
def print_on():
    print("printを押しました")

def copy_on():
    print("Copyを押しました")

def cut_on():
    print("Cutを押しました")
    
### メニューツールバーの作成 ###
menubar = tkinter.Menu(root)

###########################################################

# Fileに以下のラベルを追加
filemenu = tkinter.Menu(menubar, tearoff=0)
filemenu.add_command(label="print", command=print_on)
filemenu.add_separator() # 分割するライン
filemenu.add_command(label="Exit", command=root.quit)

# メニューバーにFileを作成
menubar.add_cascade(label="File", menu=filemenu)

###########################################################

# Editに以下のラベルを追加
editmenu = tkinter.Menu(menubar, tearoff=0)
editmenu.add_command(label="Copy", command=copy_on)
editmenu.add_separator() # 分割するライン
editmenu.add_command(label="Cut", command=cut_on)

# メニューバーにEditを追加
menubar.add_cascade(label="Edit", menu=editmenu)

###########################################################

root.config(menu=menubar)

app = Application(master=root)
app.mainloop()

def create_widgets( ) 部分

ここでメニュー画面のボタンを作っています。

##### capture section #####

ここでポップアップ画面を作っています。

### ラベルの作成 ###

ここでポップアップ画面に表示する文字を作成します。

具体的には、tkinter.Label( ) でポップアップ画面に文字を書くことができます。

### 位置調整の変数 ###

これはポップアップ画面上の文字の位置を微調整するためのものです。

文字やドロップダウンリスト、ボタンの位置を後から微調整したい場合が多いため、あらかじめ設定しておく方が便利です。

### CAPの実行 ###

ここで別プログラムを呼びます。

def click_btn_cap( ) の中で、capture_gui.cap_img(txt, cam_id, cap_size, grid_sepa, directory_path) と記載している部分があります。

この意味は、capture_gui.py という別プログラムの中で、def cap_img( ) で定義されている部分があり、それをこちらのプログラムで呼んで、カッコ内の各引数に変数を代入するという動作をします。

プログラムの最初の方で、import capture_gui と記載していた部分を覚えているでしょうか。

このコードにより、capture_gui.py という別プログラムを呼ぶことができます。

### Capture ボタン ###

ここでポップアップ画面下部のグレー色の Capture ボタンを作成しています。

tkinter.Button( ) の中で command = click_btn_cap と記載している部分があります。これは Capture ボタンが押されたとき、click_btn_cap が実行されるという意味です。

つまり、先のコードで関数化しておいた def click_btn_cap( ) が呼ばれ、capture_gui.cap_img( ) が実行されるということです。

### メニュー画面の設定 ###

ここでメニュー画面上部の帯にある 「File」や「Edit」のメニューツールバー を作成しています。

今回は、メニューツールバーで文字を選択したときに、その文字がprint( ) で出力されるようにしておきました。

アイデア次第で色々な動作をさせることができると思います。

Captureプログラム

先ほど別プログラムと言っていた部分です。

以下のコードをみてください。

# プログラム名:capture_gui.py
import os
import sys
import cv2
import datetime
import tkinter.filedialog as fd

def cap_img(t, ci, cs, gs, dp):

    # パラメータ
    cap_size = cs   # キャプチャサイズ, 1:(640 x 480), 2:(1280 x 720), 3:(1920 x 1080)       

    sepa = gs
    y_step=sepa     # 高さ方向のグリッド間隔(単位はピクセル)
    x_step=sepa     # 幅方向のグリッド間隔(単位はピクセル)

    if dp == 1:
        folder_path = "./cnn_act/capture"   # capture画像を保存するフォルダのパス
    else:
        folder_path = t

    # ciはカメラのID(0:L-815, 1:ELP_5-50mm_2MP, 2:ELP_5-50mm_8MP)
    cap = cv2.VideoCapture(ci)  

    if cap_size == 1:
        cap.set(3, 640)
        cap.set(4, 480)
        cap.set(5, 120)

    elif cap_size == 2:
        cap.set(3, 1280)
        cap.set(4, 720)
        cap.set(5, 60)

    elif cap_size == 3:
        cap.set(3, 1920)
        cap.set(4, 1080)
        cap.set(5, 30)

    if cap.isOpened() is False:
        raise("IO Error")

    while True: 
        is_ok, img = cap.read()

        print(cap.get(3), cap.get(4), cap.get(5))

        if is_ok == False:
            continue

        # 画像の切り抜き(取り込み画像を1:1にする, Y:X)
        if cap_size == 1:
            img1 = img[0:480, 80:560]

        elif cap_size == 2:
            img1 = img[0:720, 280:1000]

        elif cap_size == 3:
            img1 = img[90:990, 510:1410]

        if sepa == 0:
            cv2.imshow('cap_test', img1)

        else:
            # オリジナル画像をコピー(img1はそのまま) 
            copy_img = img1.copy()

            # 画像の縦、横サイズを抽出
            img_y,img_x=copy_img.shape[:2]  

            #横線を引く:y_stepからimg_yの手前までy_stepおきに白い(BGRすべて255)横線を引く
            copy_img[y_step:img_y:y_step, :, :] = 255

            #縦線を引く:x_stepからimg_xの手前までx_stepおきに白い(BGRすべて255)縦線を引く
            copy_img[:, x_step:img_x:x_step, :] = 255

            cv2.imshow('cap_test', copy_img)

        k = cv2.waitKey(10)

        if k == ord('e'):
            # 日付の取得
            d_today = datetime.date.today()
            dt_now = datetime.datetime.now()
            
            cv2.imwrite(folder_path + str("/") +
                       str(d_today) + str("_") +
                       str(dt_now.hour) + str("_") +
                       str(dt_now.minute) + str("_") +
                       str(dt_now.second) + '.jpg', img1)
        
        if k == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

def cap_img( ) で関数化された部分が呼ばれる部分になります。

cap_img( ) のカッコの中の変数名が先に示したプログラムと違っていることに気づいたでしょうか。

先ほどのプログラムでは、capture_gui.cap_img(txt, cam_id, cap_size, grid_sepa, directory_path) と記載されていました。

それに対し、上記コードでは、cap_img(t, ci, cs, gs, dp) と記載されています。

実は、引数となっている名前を同じにする必要はありません。

大事なのは引数の順番です。

つまり以下のように対応します。

  1. txt → t
  2. cam_id → ci
  3. cap_size → cs
  4. grid_sepa → gs
  5. directory_path → dp

知っている人には当たりまえのことですが、初心者の方は混乱する箇所かと思います。

引数の順番さえ間違えなければ引数名は分かり易い名前で良いと思います。

本Captureプログラムの特徴を簡単に書きます。

本Captureプログラムの特徴

  1. USBカメラが3台切替え可能
  2. 画像サイズは3種類(480 x 480, 720 x 720, 900 x 900)
  3. グリッド線の表示が可能(0, 25, 50, 100, 150, 200px ステップ)
  4. 保存した画像のファイル名は日付(ファイル名の重複防止)
  5. キーボードの「E」ボタンを押すと画像保存
  6. 「Q」ボタンを押すとプログラム終了

まとめ

  • Tkinter を使うと簡単にポップアップ画面が作れる。しかも軽い。
  • 別プログラムを呼ぶ場合は、" import ファイル名 " と記載する。このとき.py は記載しない。
  • capture_gui.cap_img( ) と記載すると、capture_gui.py というプログラムの中の def cap_img( ) で定義された部分を呼び出せる。
  • 呼び出した関数に変数を代入する際は、引数の順番に注意する。