前回の続き。
今回は、春のパン祭りシール点数集計の処理を組み合わせていく前に、アプリ内処理の並列化をします。
スレッドで並列処理
前回はGUI上でのキー入力でカメラ画像更新をしましたが、実際は操作なしで更新するようにしたいところ。
ということで、処理をいくつかに分けて、それぞれ並列処理で別々に動かすことで、画像の自動更新を実現します。
GUIのクラス化
並列化する前に、少しコードを整理しておきたい。
今まで見てきたTkinter参考では、tkinter
のクラスを継承してアプリケーションを作っていることがありました。
そのほうがまとまりがよさそう。
tkinter.Tk
を継承するパターン、tkinter.Frame
を継承するバターンとありましたが、
OpenCV+tkinterをやっていたサイトではtkinter.Frame
を継承していたので、そっちにしておきます。
【Python/tkinter】OpenCVのカメラ動画をCanvasに表示する | イメージングソリューション
その他参考にしたページ。
【Python】Tkinterのテンプレート - Qiita
TkinterでGUI 後書きと補足 | Python学習講座
import tkinter as tk import cv2 import numpy as np from PIL import Image, ImageOps, ImageTk
class harupan_gui(tk.Frame): TEXT_CONNECT = 'Connect ' TEXT_DISCONNECT = 'Disconnect' def __init__(self, master=None): super().__init__(master) self.pack() self.cap = cv2.VideoCapture() self.master.title('Harupan App') self.master.geometry('500x300') #### 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) 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 master.bind('<KeyPress>', self.key_handler) def event_connect(self, e): if(self.t_connect.get() == self.TEXT_CONNECT): ret = self.cap.open(f'http://{self.t_ip.get()}:{self.t_port.get()}/video') if ret: print('Camera opened') self.t_connect.set(self.TEXT_DISCONNECT) else: print('Camera open failed') else: self.cap.release() print('Camera closed') self.t_connect.set(self.TEXT_CONNECT) #### Capture and display camera image on canvas #### def update_image(self): ret, img = self.cap.read() if not ret: print('Can\'t capture image') return 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) # canvas_image.create_image(0,0,image=img, anchor=tkinter.NW) print('updated') def key_handler(self, e): print(e.keysym) if e.keysym == 'space': self.update_image()
root = tk.Tk() app = harupan_gui(master=root) app.mainloop()
Canvas size: 382,269
Camera opened
space
updated
Win_L
Shift_L
Camera closed
キー入力の最後のほうのWin_L, Shift_Lは、スクリーンショットをとったときのキー入力です。
なんとなくコードのまとまりが見えてきたかな。。。
GUIと画像取得部分の分離 (スレッド化)
上で作ったharupan_gui
の処理を分割したいと思います。
分割のしかたとしては、
※後述しますが、2つ目と3つ目はまとめないとうまくいかなかったので、実装は1つにまとめました。
という感じになります。
このように分割して並列処理することで、特にGUI上でのきっかけがなくても勝手に画像が更新されるようになります。
実装は以下のようにしました。
- マルチスレッドを使用、2つのスレッドを立てる (※GUIのメイン処理
Tk.mainloop
はメインのスレッドで動かすので、計3つの並列処理になる)cv2.VideoCapture
からの画像取得(および接続・切断)- 取得した画像の
tkinter.Canvas
への反映
- スレッド間は
Queue.queue
でデータをやり取りする- 画像データ (画像取得スレッド → 画像反映スレッド)
- 接続・切断要求 (GUIメイン処理(ボタンクリックイベント) → 画像取得スレッド)
- どちらのスレッドも、アプリ起動中はずっと回り続ける処理になるが、アプリ終了時に終了できるよう、フラグを見て回るようにしておく
- フラグ不要のやり方があればよかったが…
上で作ったharupan_gui
クラスを継承して、関数をオーバーライドする形で実装します。
参考サイト。
threading --- スレッドベースの並列処理 — Python 3.7.16 ドキュメント
queue --- 同期キュークラス — Python 3.11.2 ドキュメント
Python入門 (3) -マルチスレッド|npaka|note
Pythonのスレッド間の協調作業にはQueueを使う - Qiita
[解決!Python]クラスを継承するには:解決!Python - @IT
その他はまったポイントは以下の通り。
Tk.mainloop
は別スレッド化できない- 1つのスレッド(初期化を実施したスレッド=今のメインスレッド)内でしか動かせない
- multithreading - "RuntimeError: Calling Tcl from different appartment" tkinter and threading - Stack Overflow
- デバッグ時、立ち上げたスレッドを落としきれないことがあった
- その場合、Jupyterカーネルの再起動と上のセルの再実行が必要、なかなか面倒だった
threading.active_count()
とthreading.enumerate()
で状況確認可能- 下記コードでは、立ち上げたスレッドを終了できたことが確認できるよう、表示を追加
VideoCapture
の接続・切断と画像読み出しは同じスレッドでやらないとうまくいかない- 最初は別スレッドにしていましたが、タイミングによっては切断後に読み出しが行われることがあり
- その場合、例外が発生してスレッドが落ちてしまう
- 1つのスレッドにして、接続・切断要求がある場合は画像読み出しを行わないように実装した
import queue import threading
class harupan_gui_pp(harupan_gui): def __init__(self, master=None, img_queue_size=3): super().__init__(master) self.q_connect = queue.Queue(maxsize=0) self.q_img = 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.thread1.start() self.thread2.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: if not self.q_img.empty(): img = self.q_img.get() 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) # print('updated') def cap_process(self): while self.run_flag: if not self.q_connect.empty(): print(self.q_connect.qsize()) url = self.q_connect.get() if url == None: self.cap.release() print('Camera closed') else: if self.cap.open(url): print('Camera opened') else: print('Camera open failed') elif self.cap.isOpened(): ret, img = self.cap.read() if ret and not self.q_img.full(): self.q_img.put(img) def run_app(self): self.master.mainloop() self.run_flag = False self.thread1.join(timeout=10) self.thread2.join(timeout=10) print(f'Number of threads: {threading.active_count()}') for th in threading.enumerate(): print(' ', th) def key_handler(self, e): print(e.keysym)
root = tk.Tk() app = harupan_gui_pp(root) app.run_app()
Canvas size: 382,269
Number of threads: 7
<_MainThread(MainThread, started 13660)>
<Thread(Thread-2, started daemon 8116)>
<Heartbeat(Thread-3, started daemon 27768)>
<HistorySavingThread(IPythonHistorySavingThread, started 19020)>
<ParentPollerWindows(Thread-1, started daemon 25108)>
<Thread(thread1, started 23272)>
<Thread(thread2, started 12632)>
F7
1
Camera opened
1
Camera closed
Number of threads: 5
<_MainThread(MainThread, started 13660)>
<Thread(Thread-2, started daemon 8116)>
<Heartbeat(Thread-3, started daemon 27768)>
<HistorySavingThread(IPythonHistorySavingThread, started 19020)>
<ParentPollerWindows(Thread-1, started daemon 25108)>
カメラ接続・切断と、画像表示ができました。
画像表示は、自動で更新し続けました。ただ、ちらつきが出てしまっています。
生成したスレッドもきちんと終了できています。
ここまで
今回はここで区切りにしておきます。
今回は少し寄り道になってしまいましたが、次こそ春のパン祭りシール点数集計との組み合わせをしたいと思います。