Tkinterでカメラ映像を表示するHMIをPAGEでデザイン!簡単操作でGUIに映像を組み込む方法

「Tkinterでカメラ映像をHMIに表示したいけど、どうやってコードを書けばいいの?」

そんな悩みをお持ちのあなたへ。この記事では、Tkinterを使ってカメラ映像を表示するHMI(ヒューマンマシンインターフェース)を、GUIデザインツールPAGEで作成する方法を、初心者の方にも分かりやすく解説します。

HMIは、機械やシステムの状態を人間に分かりやすく表示したり、操作するためのインターフェースです。カメラ映像をHMIに組み込むことで、リアルタイムの状況を把握し、より直感的な操作を実現できます。

この記事を読めば、あなたも簡単にTkinterでカメラ映像を表示するHMIを作成できるようになります!

PAGEでHMIのデザインを作成

まずはPAGEを使ってHMIのデザインを作成します。

  1. PAGEを起動し、新しいプロジェクトを作成します。
  2. ウィンドウに、以下のウィジェットを追加します。
    • ボタン: 「Connect」ボタン、「Disconnect」ボタン
    • コンボボックス: カメラデバイスを選択するためのコンボボックス
    • キャンバス: カメラ映像を表示するキャンバス
    • スクロールテキスト: カメラデバイスの情報などを表示するためのテキストエリア
    [画像:PAGEで作成したHMIのデザイン例]
  3. 各ウィジェットに適切な名前、サイズ、位置などを設定します。
  4. 各ウィジェットにイベントハンドラを関連付けます。イベントハンドラは、ボタンがクリックされた時やコンボボックスの選択項目が変わった時に実行される関数です。

Pythonコードの作成

次に、PAGEで作成したデザインに基づいてPythonコードを作成します。

プログラム全貌

camerahmi.py

GUI生成コード

#! /usr/bin/env python3
#  -*- coding: utf-8 -*-
#
# GUI module generated by PAGE version 8.0
#  in conjunction with Tcl version 8.6
#    Oct 05, 2024 10:25:30 PM JST  platform: Windows NT

import sys
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.constants import *
import os.path

_location = os.path.dirname(__file__)

import camerahmi_support

_bgcolor = '#d9d9d9'
_fgcolor = '#000000'
_tabfg1 = 'black' 
_tabfg2 = 'white' 
_bgmode = 'light' 
_tabbg1 = '#d9d9d9' 
_tabbg2 = 'gray40' 

_style_code_ran = 0
def _style_code():
    global _style_code_ran
    if _style_code_ran: return        
    try: camerahmi_support.root.tk.call('source',
                os.path.join(_location, 'themes', 'default.tcl'))
    except: pass
    style = ttk.Style()
    style.theme_use('default')
    style.configure('.', font = "TkDefaultFont")
    if sys.platform == "win32":
       style.theme_use('winnative')    
    _style_code_ran = 1

class Toplevel1:
    def __init__(self, top=None):
        '''This class configures and populates the toplevel window.
           top is the toplevel containing window.'''

        top.geometry("918x487+448+236")
        top.minsize(120, 1)
        top.maxsize(3844, 1061)
        top.resizable(1,  1)
        top.title("Toplevel 0")
        top.configure(background="#d9d9d9")
        top.configure(highlightbackground="#d9d9d9")
        top.configure(highlightcolor="#000000")

        self.top = top
        self.combobox = tk.StringVar()

        _style_code()
        self.TButton1 = ttk.Button(self.top)
        self.TButton1.place(relx=0.033, rely=0.062, height=26, width=75)
        self.TButton1.configure(command=camerahmi_support.ConnectBt_on_Click)
        self.TButton1.configure(text='''Connect''')
        self.TButton1.configure(compound='left')

        self.TButton2 = ttk.Button(self.top)
        self.TButton2.place(relx=0.142, rely=0.062, height=26, width=85)
        self.TButton2.configure(command=camerahmi_support.DiconnectBt_on_Click)
        self.TButton2.configure(text='''Disconnect''')
        self.TButton2.configure(compound='left')

        self.TCombobox1 = ttk.Combobox(self.top)
        self.TCombobox1.place(relx=0.033, rely=0.164, relheight=0.06
                , relwidth=0.225)
        self.value_list = []
        self.TCombobox1.configure(values=self.value_list)
        self.TCombobox1.configure(font="-family {Yu Gothic UI} -size 9")
        self.TCombobox1.configure(textvariable=self.combobox)

        self.Canvas1 = tk.Canvas(self.top)
        self.Canvas1.place(relx=0.283, rely=0.041, relheight=0.93
                , relwidth=0.699)
        self.Canvas1.configure(background="#d9d9d9")
        self.Canvas1.configure(borderwidth="2")
        self.Canvas1.configure(highlightbackground="#d9d9d9")
        self.Canvas1.configure(highlightcolor="#000000")
        self.Canvas1.configure(insertbackground="#000000")
        self.Canvas1.configure(relief="ridge")
        self.Canvas1.configure(selectbackground="#d9d9d9")
        self.Canvas1.configure(selectforeground="black")

        self.Scrolledtext1 = ScrolledText(self.top)
        self.Scrolledtext1.place(relx=0.033, rely=0.246, relheight=0.725
                , relwidth=0.23)
        self.Scrolledtext1.configure(background="white")
        self.Scrolledtext1.configure(font="TkTextFont")
        self.Scrolledtext1.configure(foreground="black")
        self.Scrolledtext1.configure(highlightbackground="#d9d9d9")
        self.Scrolledtext1.configure(highlightcolor="#000000")
        self.Scrolledtext1.configure(insertbackground="#000000")
        self.Scrolledtext1.configure(insertborderwidth="3")
        self.Scrolledtext1.configure(selectbackground="#d9d9d9")
        self.Scrolledtext1.configure(selectforeground="black")
        self.Scrolledtext1.configure(wrap="none")

# The following code is added to facilitate the Scrolled widgets you specified.
class AutoScroll(object):
    '''Configure the scrollbars for a widget.'''
    def __init__(self, master):
        #  Rozen. Added the try-except clauses so that this class
        #  could be used for scrolled entry widget for which vertical
        #  scrolling is not supported. 5/7/14.
        try:
            vsb = ttk.Scrollbar(master, orient='vertical', command=self.yview)
        except:
            pass
        hsb = ttk.Scrollbar(master, orient='horizontal', command=self.xview)
        try:
            self.configure(yscrollcommand=self._autoscroll(vsb))
        except:
            pass
        self.configure(xscrollcommand=self._autoscroll(hsb))
        self.grid(column=0, row=0, sticky='nsew')
        try:
            vsb.grid(column=1, row=0, sticky='ns')
        except:
            pass
        hsb.grid(column=0, row=1, sticky='ew')
        master.grid_columnconfigure(0, weight=1)
        master.grid_rowconfigure(0, weight=1)
        # Copy geometry methods of master  (taken from ScrolledText.py)
        methods = tk.Pack.__dict__.keys() | tk.Grid.__dict__.keys() \
                  | tk.Place.__dict__.keys()
        for meth in methods:
            if meth[0] != '_' and meth not in ('config', 'configure'):
                setattr(self, meth, getattr(master, meth))

    @staticmethod
    def _autoscroll(sbar):
        '''Hide and show scrollbar as needed.'''
        def wrapped(first, last):
            first, last = float(first), float(last)
            if first <= 0 and last >= 1:
                sbar.grid_remove()
            else:
                sbar.grid()
            sbar.set(first, last)
        return wrapped

    def __str__(self):
        return str(self.master)

def _create_container(func):
    '''Creates a ttk Frame with a given master, and use this new frame to
    place the scrollbars and the widget.'''
    def wrapped(cls, master, **kw):
        container = ttk.Frame(master)
        container.bind('<Enter>', lambda e: _bound_to_mousewheel(e, container))
        container.bind('<Leave>', lambda e: _unbound_to_mousewheel(e, container))
        return func(cls, container, **kw)
    return wrapped

class ScrolledText(AutoScroll, tk.Text):
    '''A standard Tkinter Text widget with scrollbars that will
    automatically show/hide as needed.'''
    @_create_container
    def __init__(self, master, **kw):
        tk.Text.__init__(self, master, **kw)
        AutoScroll.__init__(self, master)

import platform
def _bound_to_mousewheel(event, widget):
    child = widget.winfo_children()[0]
    if platform.system() == 'Windows' or platform.system() == 'Darwin':
        child.bind_all('<MouseWheel>', lambda e: _on_mousewheel(e, child))
        child.bind_all('<Shift-MouseWheel>', lambda e: _on_shiftmouse(e, child))
    else:
        child.bind_all('<Button-4>', lambda e: _on_mousewheel(e, child))
        child.bind_all('<Button-5>', lambda e: _on_mousewheel(e, child))
        child.bind_all('<Shift-Button-4>', lambda e: _on_shiftmouse(e, child))
        child.bind_all('<Shift-Button-5>', lambda e: _on_shiftmouse(e, child))

def _unbound_to_mousewheel(event, widget):
    if platform.system() == 'Windows' or platform.system() == 'Darwin':
        widget.unbind_all('<MouseWheel>')
        widget.unbind_all('<Shift-MouseWheel>')
    else:
        widget.unbind_all('<Button-4>')
        widget.unbind_all('<Button-5>')
        widget.unbind_all('<Shift-Button-4>')
        widget.unbind_all('<Shift-Button-5>')

def _on_mousewheel(event, widget):
    if platform.system() == 'Windows':
        widget.yview_scroll(-1*int(event.delta/120),'units')
    elif platform.system() == 'Darwin':
        widget.yview_scroll(-1*int(event.delta),'units')
    else:
        if event.num == 4:
            widget.yview_scroll(-1, 'units')
        elif event.num == 5:
            widget.yview_scroll(1, 'units')

def _on_shiftmouse(event, widget):
    if platform.system() == 'Windows':
        widget.xview_scroll(-1*int(event.delta/120), 'units')
    elif platform.system() == 'Darwin':
        widget.xview_scroll(-1*int(event.delta), 'units')
    else:
        if event.num == 4:
            widget.xview_scroll(-1, 'units')
        elif event.num == 5:
            widget.xview_scroll(1, 'units')
def start_up():
    camerahmi_support.main()

if __name__ == '__main__':
    camerahmi_support.main()

camerahmi_support.py

内部処理コード

#! /usr/bin/env python3
#  -*- coding: utf-8 -*-
#
# Support module generated by PAGE version 8.0
#  in conjunction with Tcl version 8.6
#    Oct 05, 2024 09:33:11 PM JST  platform: Windows NT

import sys
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.constants import *

import camerahmi
import cv2
import threading
from PIL import Image, ImageTk

_debug = True # False to eliminate debug printing from callback functions.
devices = []
capture = None
is_running = True

def main(*args):
    '''Main entry point for the application.'''
    global root
    root = tk.Tk()
    root.protocol( 'WM_DELETE_WINDOW' , root.destroy)
    # Creates a toplevel widget.
    global _top1, _w1
    _top1 = root
    _w1 = camerahmi.Toplevel1(_top1)
    device_serch(_w1.TCombobox1)
    
    # Comboboxに選択イベントをバインド
    _w1.TCombobox1.bind("<<ComboboxSelected>>", device_on_selected)
    root.mainloop()

def ConnectBt_on_Click(*args):
    global _w1
    global capture
    # イベント発生元のウィジェットを取得
    widget = _w1.combobox
    selected_value = widget.get()  # イベント発生元のComboboxの選択された値を取得
    device_number = int(selected_value.split("Device ")[1])
    
    if capture is not None:
        capture.release()  # 既存のキャプチャがあれば解放
    capture = cv2.VideoCapture(device_number)
    # スレッドを起動してカメラのリードを開始
    thread = threading.Thread(target=read_camera)
    thread.start()
    
def DiconnectBt_on_Click(*args):
    global capture, is_running
    is_running = False


def read_camera():
    global capture, is_running
    global _w1
    
    is_running = True
    while is_running:
        if capture is not None and capture.isOpened():
            ret, frame = capture.read()
            if ret:
                # BGRからRGBに変換
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                
                # 画像をPIL形式に変換し、Tkinterで表示できる形式に変換
                img = Image.fromarray(frame)
                img = img.resize((640, 480))  # サイズ変更(必要に応じて)
                img_tk = ImageTk.PhotoImage(image=img)
                
                # Canvasに画像を描画
                _w1.Canvas1.create_image(0, 0, anchor=tk.NW, image=img_tk)
                _w1.Canvas1.image = img_tk  # 参照を保持する必要がある
                
    # カメラをリリースしてウィンドウを閉じる
    if capture is not None:
        capture.release()
    cv2.destroyAllWindows()
    
def device_serch( combo ):
    global devices
    current_values = list(combo["values"])

    # 利用可能なデバイスを捜索
    for i in range(10):  
        cap = cv2.VideoCapture(i)

        # デバイスが存在し、開かれた場合
        if cap.isOpened():
           
            width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
            height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
            fps = cap.get(cv2.CAP_PROP_FPS)
            autofocus = cap.get(cv2.CAP_PROP_AUTOFOCUS)
            focus = cap.get(cv2.CAP_PROP_FOCUS)
            autoexposure = cap.get(cv2.CAP_PROP_AUTO_EXPOSURE)
            exposure = cap.get(cv2.CAP_PROP_EXPOSURE)
            iso_speed = cap.get(cv2.CAP_PROP_ISO_SPEED)
            cv2.waitKey(0)
            cap.release()
            
             # デバイスをリストに追加
            devices.append({
                "index": i,
                "width": width,
                "height": height,
                "fps": fps,
                "autofocus":autofocus,
                "focus":focus,
                "autoexposure":autoexposure,
                "exposure":exposure,
                "iso_speed":iso_speed,
            })
            new_value = f"Device {i}"
            updated_values = current_values + [new_value]
        else:
            pass

    combo["values"] = updated_values
    cv2.destroyAllWindows()
    
def device_on_selected(event):
    global _w1
    # イベント発生元のウィジェットを取得
    widget = event.widget
    selected_value = widget.get()  # イベント発生元のComboboxの選択された値を取得
    #print(f"Selected value from {widget}: {selected_value}")
    
    device_number = int(selected_value.split("Device ")[1])
    
    device = devices[device_number]
    device_info = (
            f"Index: {device['index']}, \n"
            f"Resolution: {device['width']}x{device['height']}, \n"
            f"FPS: {device['fps']}, \n"
            f"Autofocus: {device['autofocus']}, \n"
            f"Focus: {device['focus']}, \n"
            f"Autoexposure: {device['autoexposure']}, \n"
            f"Exposure: {device['exposure']}, \n"
            f"ISO Speed: {device['iso_speed']}\n"
        )
    _w1.Scrolledtext1.delete(1.0, 'end')  # 全削除(0から末尾まで)
    _w1.Scrolledtext1.insert('end', device_info)  # 新しいテキストを挿入
    
    
if __name__ == '__main__':
    camerahmi.start_up()

プログラム詳細

カメラ映像の表示

カメラから取得した映像は、OpenCVで扱うBGR形式になっています。TkinterのCanvasで表示するためには、RGB形式に変換する必要があります。また、Tkinterで表示するためにはPILのImage形式に変換しています。

また、この処理をスムーズに行うために、read_camera関数は別スレッドで実行されます。これにより、GUIの動作がブロックされずに、カメラ映像の表示が継続的に行うことができます。

def read_camera():
    global capture, is_running
    global _w1  # Canvas1 が定義されているウィジェットへの参照

    is_running = True
    while is_running:
        if capture is not None and capture.isOpened():
            ret, frame = capture.read()
            if ret:
                # BGRからRGBに変換
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                
                # 画像をPIL形式に変換し、Tkinterで表示できる形式に変換
                img = Image.fromarray(frame)
                img = img.resize((640, 480))  # サイズ変更(必要に応じて)
                img_tk = ImageTk.PhotoImage(image=img)
                
                # Canvasに画像を描画
                _w1.Canvas1.create_image(0, 0, anchor=tk.NW, image=img_tk)
                _w1.Canvas1.image = img_tk  # 参照を保持する必要がある
                
    # カメラをリリースしてウィンドウを閉じる
    if capture is not None:
        capture.release()
    cv2.destroyAllWindows()

イベントハンドラの作成

  • device_on_selected: コンボボックスでカメラデバイスが選択された際に、選択されたデバイスの情報をスクロールテキストに表示します。
  • ConnectBt_on_Click: 「Connect」ボタンがクリックされた際に、選択されたカメラデバイスに接続し、read_camera() 関数を別スレッドで実行してカメラ映像の表示を開始します。
  • DiconnectBt_on_Click: 「Disconnect」ボタンがクリックされた際に、read_camera() 関数を停止し、カメラ接続を切断します。

実行

作成したPythonコードを実行すると、カメラ映像がHMI上に表示されます。
以下のように表示されます。

まとめ

本記事では、TkinterとPAGEを使ってカメラ映像を表示するHMIを作成する方法を紹介しました。

  • PAGEでGUIデザインを作成し、イベントハンドラを関連付けることで、直感的なHMIを作成できます。
  • TkinterとOpenCVを組み合わせることで、カメラ映像をGUIに簡単に組み込むことができます。
  • 別スレッドでカメラ映像を取得することで、GUIの動作をスムーズに保つことができます。

この方法を活用することで、様々な用途のHMIを簡単に開発することができます。ぜひ試してみてください!

この記事が、あなたのTkinterを使ったHMI開発の役に立てば幸いです!

Pythonの学習には、こちらを参考にしてみてください

にいやん

出身 : 関西 居住区 : 関西 職業 : 組み込み機器エンジニア (エンジニア歴13年) 年齢 : 38歳(2022年11月現在) 最近 業務の効率化で噂もありPython言語に興味を持ち勉強しています。 そこで学んだことを記事にして皆さんとシェアさせていただければと思いブログをはじめました!! 興味ある記事があれば皆さん見ていってください!! にほんブログ村