「Tkinterでカメラ映像をHMIに表示したいけど、どうやってコードを書けばいいの?」
そんな悩みをお持ちのあなたへ。この記事では、Tkinterを使ってカメラ映像を表示するHMI(ヒューマンマシンインターフェース)を、GUIデザインツールPAGEで作成する方法を、初心者の方にも分かりやすく解説します。
HMIは、機械やシステムの状態を人間に分かりやすく表示したり、操作するためのインターフェースです。カメラ映像をHMIに組み込むことで、リアルタイムの状況を把握し、より直感的な操作を実現できます。
この記事を読めば、あなたも簡単にTkinterでカメラ映像を表示するHMIを作成できるようになります!
まずはPAGEを使ってHMIのデザインを作成します。
次に、PAGEで作成したデザインに基づいてPythonコードを作成します。
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()
内部処理コード
#! /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()
作成したPythonコードを実行すると、カメラ映像がHMI上に表示されます。
以下のように表示されます。
本記事では、TkinterとPAGEを使ってカメラ映像を表示するHMIを作成する方法を紹介しました。
この方法を活用することで、様々な用途のHMIを簡単に開発することができます。ぜひ試してみてください!
この記事が、あなたのTkinterを使ったHMI開発の役に立てば幸いです!
Pythonの学習には、こちらを参考にしてみてください