今回は、春のパン祭り点数集計GUIの微調整をしていきます。
調整項目
主に調整したいのは、
- 画像更新停止ボタン追加
ある程度点数認識できたら間違っている部分を目視で確認、点数修正、という使い方を想定しているので。 - ウィンドウサイズに応じた表示サイズ調整
目視確認しやすいウィンドウサイズに調整したいので。
の2点。
あと細かい調整。
- 点数集計結果は
Label
ウィジェットを使って表示
前までは、画像に点数表示エリアをくっつけて表示していましたが、今はGUIでの表示手段があるのでそれで。 cv2.VideoCapture
のopen()
のタイムアウト設定
open()
やread()
に失敗したときは、接続ボタンを'Connect'に戻すようにもしておきました。- ウィジェットのレイアウト微調整
grid()
でレイアウトする際、padx
の引数で隙間を空けました。
【tkinter】grid()を使ったWidgetの配置の方法 - どん底から這い上がるまでの記録 - ログ表示の追加
GUI上で接続失敗が分かるように。
今回はLabel
ウィジェットを使いましたが、複数行になるときはText
を使う必要があるよう。 - キー入力ハンドラは不要になったので削除
実行結果
コードが長いので、先に結果のgifを出します。
コード
今回は変更したコード全体と実行結果だけ掲載します。詳細の解説は省略で…
from harupan_data.harupan import *
import tkinter as tk import cv2 from PIL import Image, ImageOps, ImageTk import queue import threading
class harupan_gui(tk.Frame): TEXT_CONNECT = 'Connect ' TEXT_DISCONNECT = 'Disconnect' TEXT_STOP = 'Stop ' TEXT_RESUME = 'Resume' def __init__(self, master=None, img_queue_size=1, svm_data='harupan_data/harupan_svm_220412.dat', template_data='harupan_data/templates2021.json'): super().__init__(master) self.cap = cv2.VideoCapture() self.open_params = (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 10000, cv2.CAP_PROP_READ_TIMEOUT_MSEC, 5000) self.svm = load_svm(svm_data) self.templates = load_templates(template_data) #### Main window settings #### self.master.title('Harupan App') self.master.geometry('500x400') self.master.protocol('WM_DELETE_WINDOW', self.cleanup_app) self.master.bind('<Configure>', self.update_canvas_size) #### Sub frames #### self.frame_connection = tk.Frame(self) self.frame_log = tk.Frame(self.frame_connection, width=120, height=30) self.frame_log.propagate(False) self.frame_canvas = tk.Frame(self) self.frame_canvas.config(relief='ridge', bd=5) self.frame_result = tk.Frame(self) #### Entries for connection information #### self.t_ip = tk.StringVar(value='192.168.1.7') self.t_port = tk.StringVar(value='4747') self.entry_ip = tk.Entry(self.frame_connection, textvariable=self.t_ip) self.entry_port = tk.Entry(self.frame_connection, textvariable=self.t_port) #### Connect button #### self.t_connect = tk.StringVar(value=self.TEXT_CONNECT) self.button_connect = tk.Button(self.frame_connection, textvariable=self.t_connect) self.button_connect.bind('<Button-1>', self.event_connect) #### Connection log #### self.t_log = tk.StringVar() self.label_log = tk.Label(self.frame_log, textvariable=self.t_log) #### Image canvas #### self.canvas_image = tk.Canvas(self.frame_canvas, bg='white') self.disp_img = None #### Label for calculation result #### self.t_calc_result = tk.StringVar(value=' 0 points') self.label_points = tk.Label(self.frame_result, bg='black', fg='green', font=('Consolas', 20), textvariable=self.t_calc_result) #### Stop button #### self.t_stop = tk.StringVar(value=self.TEXT_STOP) self.button_stop = tk.Button(self.frame_result, textvariable=self.t_stop) self.button_stop.bind('<Button-1>', self.event_stop_button) #### Place widgets #### self.pack(expand=True, fill='both') self.frame_connection.pack() self.frame_canvas.pack(expand=True, fill='both') self.frame_result.pack() self.entry_ip.grid(row=0, column=0) self.entry_port.grid(row=1, column=0) self.button_connect.grid(row=0, column=1, rowspan=2, padx=(5,0)) self.frame_log.grid(row=0, column=2, rowspan=2, padx=(5,0)) self.label_log.pack(fill='both') self.canvas_image.pack(expand=True, fill='both') self.label_points.grid(row=0, column=0) self.button_stop.grid(row=0, column=1, padx=(5,0)) self.frame_canvas.update() self.w, self.h = self.canvas_image.winfo_width(), self.canvas_image.winfo_height() print(f'Canvas size: {self.w},{self.h}') #### Start internal threads #### self.q_connect = queue.Queue(maxsize=0) self.q_img = queue.Queue(maxsize=1) self.q_img2 = queue.Queue(maxsize=img_queue_size) self.run_flag = True self.thread1 = threading.Thread(target=self.update_image, name='thread1') self.thread2 = threading.Thread(target=self.cap_process, name='thread2') self.thread3 = threading.Thread(target=self.calc_process, name='thread3') self.thread1.start() self.thread2.start() self.thread3.start() print(f'Number of threads: {threading.active_count()}') for th in threading.enumerate(): print(' ', th) def event_connect(self, e): self.t_log.set('') if(self.t_connect.get() == self.TEXT_CONNECT): url = f'http://{self.t_ip.get()}:{self.t_port.get()}/video' self.q_connect.put(url) self.t_connect.set(self.TEXT_DISCONNECT) else: self.q_connect.put(None) self.t_connect.set(self.TEXT_CONNECT) def update_image(self): while self.run_flag: val, img = self.q_img2.get() if not val: continue img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = Image.fromarray(img) img = ImageOps.pad(img, (self.w,self.h)) self.disp_img = ImageTk.PhotoImage(image=img) self.canvas_image.create_image(self.w/2,self.h/2,image=self.disp_img) def _print_log(self, mes): print(mes) self.t_log.set(mes) def cap_process(self): while self.run_flag: if not self.q_connect.empty(): url = self.q_connect.get() if url == None: self.cap.release() self._print_log('Camera closed') elif self.cap.open(url, cv2.CAP_FFMPEG, self.open_params): self._print_log('Camera opened') else: self._print_log('Camera open failed') self.t_connect.set(self.TEXT_CONNECT) elif self.cap.isOpened(): ret, img = self.cap.read() if not ret: self._print_log('Can\'t receive frame') self.cap.release() self.t_connect.set(self.TEXT_CONNECT) elif not self.q_img.full(): self.q_img.put((True, img)) def calc_process(self): while self.run_flag: val, img = self.q_img.get() if not val: self.q_img2.put((False, None)) continue if self.t_stop.get() == self.TEXT_STOP: score, img2 = calc_harupan(img, self.templates, self.svm) self.t_calc_result.set(f'{score:2.1f} points') if not self.q_img2.full(): self.q_img2.put((True, img2)) def event_stop_button(self, e): s = self.TEXT_RESUME if self.t_stop.get() == self.TEXT_STOP else self.TEXT_STOP self.t_stop.set(s) def update_canvas_size(self, e): self.w, self.h = self.canvas_image.winfo_width(), self.canvas_image.winfo_height() def cleanup_app(self): self.run_flag = False # Put dummy data to finish thread1(update_image()), thread3(calc_process()) if self.q_img.empty(): self.q_img.put((False, None)) if self.q_img2.empty(): self.q_img2.put((False, None)) self.thread1.join(timeout=10) self.thread2.join(timeout=10) self.thread3.join(timeout=10) print(f'Number of threads: {threading.active_count()}') for th in threading.enumerate(): print(' ', th) if self.cap.isOpened(): self.cap.release() self.master.destroy()
root = tk.Tk() app = harupan_gui(root) app.mainloop()
Canvas size: 490,314
Number of threads: 8
<_MainThread(MainThread, started 23564)>
<Thread(Thread-2, started daemon 14256)>
<Heartbeat(Thread-3, started daemon 25136)>
<HistorySavingThread(IPythonHistorySavingThread, started 19276)>
<ParentPollerWindows(Thread-1, started daemon 5052)>
<Thread(thread1, started 22068)>
<Thread(thread2, started 16572)>
<Thread(thread3, started 21688)>
Camera opened
Camera closed
Number of threads: 5
<_MainThread(MainThread, started 23564)>
<Thread(Thread-2, started daemon 14256)>
<Heartbeat(Thread-3, started daemon 25136)>
<HistorySavingThread(IPythonHistorySavingThread, started 19276)>
<ParentPollerWindows(Thread-1, started daemon 5052)>
Frame
、Canvas
ウィジェットを、pack()
でexpand=True
、fill='both'
を設定して配置すると、マウスでウィンドウサイズを変更したときに自動で追従してリサイズしてくれた- メインウィンドウの設定変更時のハンドラでサイズ調整しようとしていたが、不要だった
pack()
の引数のexpand
とfill
の違いは、占有領域を広げるだけにするか、ウィジェット自体の大きさも合わせて広げるか 【Python/tkinter】ウィジェットの配置(pack) | イメージングソリューション
以上
まだGUIで直したいところは色々ありますが、きりがないのでひとまずこれでフィックスで。
次回はexe化になると思われます。
その他参考
コラム - ゼロから歩くPythonの道 | 第21回 tkinterでフォントと文字サイズ、色を変更する方法|CTC教育サービス 研修/トレーニング
【Python/Tkinter】Label(ラベル)の使い方:文字フォント・サイズ・色・配置の設定 | OFFICE54