今回は、今まで作ったGUIに、点数集計処理を組み合わせていきます。
処理組み合わせ
去年の点数集計処理と、今回のGUIを組み合わせるだけです。
前回はtkinter.Frame
を継承したアプリを一度作り、その後で関数をオーバーライドしたりしましたが、今回は最終的な形のものをまとめて作ります。
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' def __init__(self, master=None, img_queue_size=3, svm_data='harupan_data/harupan_svm_220412.dat', template_data='harupan_data/templates2021.json'): super().__init__(master) self.pack() self.cap = cv2.VideoCapture() self.svm = load_svm(svm_data) self.templates = load_templates(template_data) self.master.title('Harupan App') self.master.geometry('500x400') #### 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, textvariable=self.t_ip) self.entry_port = tk.Entry(self, textvariable=self.t_port) self.entry_ip.grid(row=0, column=0) self.entry_port.grid(row=1, column=0) #### Connect button #### self.t_connect = tk.StringVar(value=self.TEXT_CONNECT) self.button_connect = tk.Button(self, textvariable=self.t_connect) self.button_connect.bind('<Button-1>', self.event_connect) self.button_connect.grid(row=0, column=1) #### Image canvas #### self.canvas_image = tk.Canvas(self, bg='white') self.canvas_image.grid(row=2,column=0, columnspan=2) self.update() self.w, self.h = self.canvas_image.winfo_width(), self.canvas_image.winfo_height() print(f'Canvas size: {self.w},{self.h}') self.disp_img = None self.master.bind('<KeyPress>', self.key_handler) self.master.protocol('WM_DELETE_WINDOW', self.cleanup_app) 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): 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 cap_process(self): while self.run_flag: if not self.q_connect.empty(): url = self.q_connect.get() if url == None: self.cap.release() print('Camera closed') elif self.cap.open(url): print('Camera opened') else: print('Camera open failed') elif self.cap.isOpened(): ret, img = self.cap.read() if not ret: print('Can''t receive frame') 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 score, img2 = calc_harupan(img, self.templates, self.svm) score_text = str(score) + ' points' score_area = np.zeros((50, img2.shape[1], 3), 'uint8') score_area = cv2.putText(score_area, score_text, (0,45), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,0), 3) img2 = np.vstack((img2, score_area)) if not self.q_img2.full(): self.q_img2.put((True, img2)) 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() def key_handler(self, e): print(e.keysym)
root = tk.Tk() app = harupan_gui(root) app.mainloop()
Canvas size: 382,269
Number of threads: 8
<_MainThread(MainThread, started 20972)>
<Thread(Thread-2, started daemon 10616)>
<Heartbeat(Thread-3, started daemon 6208)>
<HistorySavingThread(IPythonHistorySavingThread, started 18536)>
<ParentPollerWindows(Thread-1, started daemon 17772)>
<Thread(thread1, started 5064)>
<Thread(thread2, started 22324)>
<Thread(thread3, started 17676)>
F7
BackSpace
1
4
Camera opened
Camera closed
Number of threads: 5
<_MainThread(MainThread, started 20972)>
<Thread(Thread-2, started daemon 10616)>
<Heartbeat(Thread-3, started daemon 6208)>
<HistorySavingThread(IPythonHistorySavingThread, started 18536)>
<ParentPollerWindows(Thread-1, started daemon 17772)>
キー入力のうち、F7はキャプチャツールの録画開始キー、あとはIPアドレスが変わったのでその入力。
更新レートは2~3秒と、あまりよろしくない。
調整した点がいくつか。
- ウィンドウサイズを500x300 → 500x400に変更
- 表示画像の下側が切れてしまっていたため
- 固定サイズではなくうまく表示する方法があればいいですが、これは後で再検討で。
- 点数計算処理は、新しいスレッド処理として追加
- 最初は画像取得のスレッド内で実施しましたが、そうすると、カメラの動きに対して追従性が悪い。
cv2.VideoCapture
の中のバッファに画像データがある程度溜め込まれるためかと推定。 - 画像取得のスレッドは、ひたすら
read()
関数を呼んで、前のフレームの点数計算処理が終わっていなければ画像を捨てる、という仕事としました。 - これにより、処理対象の画像が、リアルタイムに取得したものになり、追従性が改善しました。
- 最初は画像取得のスレッド内で実施しましたが、そうすると、カメラの動きに対して追従性が悪い。
update_image()
関数のループの中で、q_img2.get()
を呼ぶ前の、q_img2
が空でないかどうか(q_img2.empty()
がFalse
かどうか)のチェックを削除- 前回やったところでは、この確認をしないと、アプリ終了時に
update_image()
関数のスレッドがq_img
のデータ待ちのまま終了できなかったので。 - ただ、今回これで動かしてみると、点数集計処理が全然終わらない…
- おそらく、このスレッドが
q_img2.empty()
を確認するだけの処理を繰り返して、CPU時間がそちらに取られてしまったのかと。 - これを外すと、きちんと点数集計処理が完了するようになりました。
- ただし、アプリ終了時にスレッドを落とせるよう、
q_img
,q_img2
にダミーデータを入れる対応をしています。
- 前回やったところでは、この確認をしないと、アプリ終了時に
- アプリ終了時の後処理を定義して、
Tk().protocol()
で登録- 前回は、自分で実装した
run_app()
関数の中で、mainloop()
を呼んで、その後に後処理(run_flag
を落とす、並列処理スレッドの終了待ち)をやっていました。 - しかし、一部処理で、アプリが終了した後に実行するとエラーが出るものがありました(
update_image
内のImageTk.PhotoImage()
)。 - ということで、後処理の自作と登録を行うようにしました。
- なお、元の
run_app()
関数は、mainloop()
しかやることがなくなったので廃止。
- 前回は、自分で実装した
ここまで
GUIで春のパン祭り点数集計ができるようになりました。
使ってみて、GUIでもう少し改善したいところがあるので、次回それをやっておきたいなと。
あとはexe化とGitHubでの公開。