PythonでGUI画面を作る方法::PySimpleGUIでグリッド表示機能のついたキャプチャ画面を作る

2020年9月20日

 

si-sa

今日はPySimpleGUIを使ってキャプチャ画面を作る方法を紹介します。

PySimpleGUIとは

簡単にGUI画面が作れるライブラリです。

Tkinter でも簡単なGUI画面が作成できますが見た目が簡素すぎます。

それに比べてPySimpleGUIは見た目がカッコ良いです。

素人でもプロっぽいGUI画面が作成できます。個人的にはかなりオススメです。

ホームページに詳しい使い方やレシピが乗っているので参照してください。

インストールの仕方

以下のコマンドで簡単にインストールできます。

pip install pysimplegui

キャプチャ画面の作り方

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

import cv2
import time
import datetime
import numpy as np
import tkinter as Tk
import PySimpleGUI as sg
from matplotlib import pyplot as plt
from PIL import ImageFont, ImageDraw, Image

### window の色を設定 ###
sg.theme('Dark Blue 3')

# パラメータ設定
no = 0
choices_1 = ('640 x 480', '1280 x 720', '1920 x 1080')
choices_2 = ('50', '100', '150')

### ボタン関係の設定 ###

# テキスト設定
s_moji = sg.Text('Capture Size', size=(15, 1), font=('メイリオ', 13))
s_sen_1 = sg.Text('_'  * 36)
s_sen_2 = sg.Text('_'  * 36)

# 隠れボタン設定
s_choice_1 = sg.InputCombo(choices_1,
                            default_value='1920 x 1080',
                            key= '-cap size-',
                            size=(20, len(choices_1)),
                            enable_events=True)

s_choice_2 = sg.InputCombo(choices_2,
                            default_value='選択してください',
                            key= '-grid sepa size-',
                            size=(20, len(choices_2)),
                            enable_events=True)

# ラジオボタン設定
s_Radio_1 = sg.Radio('Grid ON', "RADIO1", default=False, size=(8,1), font=('メイリオ', 13), key='-grid on-')
s_Radio_2 = sg.Radio('Grid OFF', "RADIO1", default=True, size=(8, 1), font=('メイリオ', 13),key='-grid off-')
s_Radio_3 = sg.Radio('Grid DIRECT', "RADIO1", default=False, size=(19, 1), font=('メイリオ', 13),key='-grid direct-')

# スライダー設定
s_Slider_1 = sg.Slider((0, 255), 128, 1, orientation='h', size=(32, 15), key='-grid sepa-')

# 実行ボタン設定
s_button_1 = sg.Submit(button_text='Capture', size=(15, 2))
s_button_2 = sg.Submit(button_text='Quit', size=(15, 2))


### 画面レイアウト設定 ###
""" 設定パラメータはフレームでまとめた方が便利 """

layout_0 = sg.Frame('',
                    [
                     [s_moji],
                     [s_choice_1],
                     [s_sen_1],
                     [sg.Frame(layout=[[s_Radio_1, s_Radio_2]],
                                title='Grid spacing 1',
                                title_color='Red',
                                font=('メイリオ', 10),
                                relief=sg.RELIEF_SUNKEN
                               )],
                     [s_Slider_1],
                     [sg.Frame(layout=[[s_Radio_3]],
                                title='Grid spacing 2',
                                title_color='Red',
                                font=('メイリオ', 10),
                                relief=sg.RELIEF_SUNKEN
                               )],
                     [s_choice_2],
                     [s_sen_2],     
                     [s_button_1, s_button_2]
                    ],
                    relief=sg.RELIEF_SUNKEN
                   )

### レイアウト設定 ###
layout_1 = [
            [sg.Image(filename='', key='-IMAGE-'), layout_0]
           ]

### ウィンドウ生成 ###
window = sg.Window('OpenCV Image', layout_1, location=(300, 10))


### capture セクション ###
cap = cv2.VideoCapture(0)

cap.set(3, 1920)
cap.set(4, 1080)
cap.set(5, 30)


### イベントループ ###
while True:
    # 時間測定開始
    t1 = time.perf_counter()

    event, values = window.read(timeout=20)

    if event == 'Quit' or event == sg.WIN_CLOSED:
        break

    _, frame = cap.read()

    frame_1 = frame[90:990, 510:1410]

    if event is None:
        print('exit')
        break
    
    if values['-cap size-']:
        if choices_1[0] == values['-cap size-'][:]:
            frame_1 = frame[0:480, 80:560]
        elif choices_1[1] == values['-cap size-'][:]:
            frame_1 = frame[0:720, 280:1000]
        elif choices_1[2] == values['-cap size-'][:]:
            frame_1 = frame[90:990, 510:1410]

    if values['-grid on-']:
        if values['-grid sepa-']:
            
            # 画像の縦、横サイズを抽出
            img_y, img_x = frame_1.shape[:2]

            # 横線を引く:int(values['-grid sepa-'])から
            # img_yの手前までint(values['-grid sepa-'])おきに白い(BGRすべて255)横線を引く
            frame_1[int(values['-grid sepa-']):img_y:int(values['-grid sepa-']), :, :] = 255

            # 縦線を引く:int(values['-grid sepa-'])から
            # img_xの手前までint(values['-grid sepa-'])おきに白い(BGRすべて255)縦線を引く
            frame_1[:, int(values['-grid sepa-']):img_x:int(values['-grid sepa-']), :] = 255

    elif values['-grid direct-']:
        if choices_2[0] == values['-grid sepa size-'][:]:
            img_y, img_x = frame_1.shape[:2]
            frame_1[int(values['-grid sepa size-']):img_y:int(values['-grid sepa size-']), :, :] = 255
            frame_1[:, int(values['-grid sepa size-']):img_x:int(values['-grid sepa size-']), :] = 255

        elif choices_2[1] == values['-grid sepa size-'][:]:
            img_y, img_x = frame_1.shape[:2]
            frame_1[int(values['-grid sepa size-']):img_y:int(values['-grid sepa size-']), :, :] = 255
            frame_1[:, int(values['-grid sepa size-']):img_x:int(values['-grid sepa size-']), :] = 255

        elif choices_2[2] == values['-grid sepa size-'][:]:
            img_y, img_x = frame_1.shape[:2]
            frame_1[int(values['-grid sepa size-']):img_y:int(values['-grid sepa size-']), :, :] = 255
            frame_1[:, int(values['-grid sepa size-']):img_x:int(values['-grid sepa size-']), :] = 255


    ### 画像の保存 ###
    if event == 'Capture':

        # 日付の取得
        d_today = datetime.date.today()
        dt_now = datetime.datetime.now()
            
        cv2.imwrite('./cnn_act/capture' + str("/") + str(no) + str("_") +
                    str(d_today) + str("_") +
                    str(dt_now.hour) + str("_") +
                    str(dt_now.minute) + str("_") +
                    str(dt_now.second) + '.jpg', frame_1)

        no += 1

    ### FPS 計算 ###
    elapsedTime = time.perf_counter() - t1
    fps = "{:.0f}FPS".format(1/elapsedTime)

    # 画面にfpsを表示
    frame_1 = cv2.putText(frame_1, fps, (15, 35),
                          cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 255), 2, cv2.LINE_AA)

    # 映像をpng画像に変換してwindow画面を更新
    imgbytes = cv2.imencode('.png', frame_1)[1].tobytes()
    window['-IMAGE-'].update(data=imgbytes)

    if event == 'Quit':
        print('Quit')
        break

# ウィンドウの破棄と終了
window.close()

ポイントの説明

PySimplGUIを使う上でのポイントを説明します。

画面に表示させるものは前もってまとめて定義しておく

テキスト、ライン、ボタン、スライダーなど。

それらをリスト [ ] の中に入れ、重ねて行けばそのまま画面が出来上がります。

画面レイアウトを考える

ソースコードの中で、layout_0の部分をみてください。

この中で、sg.Frame( ) の中に、たくさんのリスト [ ] が重なっているのが分かると思います。

この sg.Frame( ) は一つのレイアウトの中に別なレイアウトを作る役割をしている部分です。

以下のイメージ図をみてください。

layout_image

sg.Image( ) の中にはWeb cameraの映像が表示されます。

layout_0 の部分には、テキスト、ライン、ボタン、スライダーが配置されます。

表示させる要素が複数ある場合は、sg.Frame( ) を使ってリスト[ ] で重ねます。なお、ボタン類の入れ替えは自由です。画面のレイアウトを考えてからコードを作る方が良いと思います。

ちなみに、一度定義したテキスト、ライン、ボタン、スライダーを2回使用する(呼び出す)ことはできません。

使用したい場合は、同じものを2つ定義してください。

windowの表示画像を更新するには png 画像への変換が必要

Open CV を使って Web camera で撮影した画像データは ndarray 型になっています。

実は、このデータ型のままではwindow画面に画像を表示させることはできません。

window画面に画像を表示させるためには、Web cameraで撮影した画像データを 一旦 png 画像に変換する必要があります。

以下がその部分です。

imgbytes = cv2.imencode('.png', frame_1)[1].tobytes()
window['-IMAGE-'].update(data=imgbytes) 

frame_1 を imencode( ) 関数でpng 画像に変換・圧縮し、メモリバッファに保存します。

次に、window[ ] の ' -IMAGE- ' キーの部分を先ほどメモリバッファに保存した画像で update します。

これにより、window画面に画像が表示され、また更新される仕組みとなっています。

実行画面

実行するとこんな画面になります。

si-sa_no_grid

Web camera のキャプチャーサイズは、1920×1080 ピクセルとしています。

ただし、画面表示は 900×900ピクセルサイズとしています。これは、グリッド表示させる際に正方形画面の方が都合が良いためです。

この画面で「Capture」ボタンを押すと以下の画像がキャプチャされます。

画像サイズは 900×900ピクセルです。拡張子は、jpg です。

ソースコードの cv2.imwrite( ) 中に、拡張子を指定している部分があります。

この部分を変えれば(例:’ . png ' など)保存する画像の拡張子を変えることができます。

次に、グリッドの表示方法について説明します。

グリッドを表示させるには「Grid ON」を選択します。すると次のような画面になります。

表示されたグリッドはスライドバーで間隔を変えることができます。

スライドバーの値が 128のとき(デフォルト値)
スライドバーの値が 78のとき

スライドバーの値は、最大が 255、最小が 0となっています。

ソースコードの中の sg.Slider( ) の 第一引数(0, 255)の部分を変えれば、最大・最小値を変えることができます。

次に、「Grid DIRECT」を選択するとグリッドを一定間隔で表示させることができます。

実行すると以下のような画面になります。

この画面では 100ピクセルごとにグリッド線が表示されています。

ソースコードの choices_2 で、選択できるグリッド幅を指定しています。

ここを変えれば、好きなグリッド幅が表示できるようになります。

また、Capture Size を 640×480に変更すると以下の画面になります。

この画面で「Capture」ボタンを押すと400×400ピクセルの画像がキャプチャできます。

キャプチャした画像は以下です。

まとめ

  • PySimpleGUIを使うと簡単にプロっぽいGUI画面が作成できる。
  • 一度定義したテキスト、ライン、ボタン、スライダーを2回使用することはできない。
  • windowの表示画像を更新するには png 画像への変換が必要。

慣れれば、Tkinter より使いやすいです。

かなりオススメなので気になる方は一度使ってみてください。

おすすめ本

本プログラム作成にあたり参考にした本を紹介します。なお、このプログラムに限らず基本が大事です。何度も繰り返し読むことで理解が深まりアイデアも湧きやすくなります。

以下の本は私が何度も読み込んでいる本です。

Pythonチュートリアルの方は、プログラミング言語 Python の作者Guido さんが書き下ろした入門者のための手引き書です。一度は読むべき本だと思います。

また、入門 Python 3 の方は基本構文が豊富に掲載されています。プログラミングする際、とても参考になります。この本をみれば不明点は大体わかると思います。

基本的にO’REILLY(オライリー・ジャパン)の本は高いです。ですが、この本にヒントやエッセンスの全てが詰まっていると思います。

今回使用したWeb camera

今回の記事で使用したWeb camera を紹介します。

2種類あり、何れもELP製のカメラですがレンズの焦点距離が違います。

下の写真の左側のカメラは、焦点距離が5-50mmで、比較的遠くのものを撮影するのに向いています。

一方、下の写真の右側のカメラは焦点距離が2.8-12mmで、比較的近くのものを広範囲に撮影するのに向いています。

何れのカメラもOpenCVでの画像取り込みが可能です。

おすすめポイント

  • どちらのカメラも200万画素(1920×1080ピクセル)と手ごろな画素数(注1)
  • フォーカス、焦点距離の調整がマニュアル(注2)
  • レンズマウントが一般的なCマウント(注3)
  • フレームレートが高い(100fpsもいける)(注4)
  • ケーブル長さが3mと十分な長さ
  • 多少暗くても撮影可能
  • USB2.0で接続できる

注1.画素数が多すぎるとフレームレートの低下やノイジーな画像になるなどデメリットの方が多い。

注2.オートフォーカス仕様のWeb camera は、周囲のちょっとした明るさの変化や動きでフォーカスが動いてしまいがち。ボケボケの映像になりやすいのでストレスが溜まりやすい。

注3.Cマウントが使えるレンズなら交換可能。ちなみに、5-50mmレンズと2.8-12mmレンズの交換は可能。なので、カメラを1台購入して後から焦点距離の違うレンズだけを購入すれば、目的に応じて使い分けることも可能。私はそうしています。

注4.AI で物体検出に使う場合、フレームレートが高い方が利用範囲が広がる。