「Tkinterでカメラ映像をHMIに表示したいけど、どうやってコードを書けばいいの?」
そんな悩みをお持ちのあなたへ。この記事では、Tkinterを使ってカメラ映像を表示するHMI(ヒューマンマシンインターフェース)を、GUIデザインツールPAGEで作成する方法を、初心者の方にも分かりやすく解説します。
HMIは、機械やシステムの状態を人間に分かりやすく表示したり、操作するためのインターフェースです。カメラ映像をHMIに組み込むことで、リアルタイムの状況を把握し、より直感的な操作を実現できます。
この記事を読めば、あなたも簡単にTkinterでカメラ映像を表示するHMIを作成できるようになります!
PAGEでHMIのデザインを作成
まずはPAGEを使ってHMIのデザインを作成します。
- PAGEを起動し、新しいプロジェクトを作成します。
- ウィンドウに、以下のウィジェットを追加します。
- ボタン: 「Connect」ボタン、「Disconnect」ボタン
- コンボボックス: カメラデバイスを選択するためのコンボボックス
- キャンバス: カメラ映像を表示するキャンバス
- スクロールテキスト: カメラデバイスの情報などを表示するためのテキストエリア
- 各ウィジェットに適切な名前、サイズ、位置などを設定します。
- 各ウィジェットにイベントハンドラを関連付けます。イベントハンドラは、ボタンがクリックされた時やコンボボックスの選択項目が変わった時に実行される関数です。
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の学習には、こちらを参考にしてみてください
コメント