勉強しないとな~blog

ちゃんと勉強せねば…な電気設計エンジニアです。

OpenCVやってみる - 50. 点数集計処理組み合わせ

今回は、今まで作った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()で登録

ここまで

GUIで春のパン祭り点数集計ができるようになりました。

使ってみて、GUIでもう少し改善したいところがあるので、次回それをやっておきたいなと。

あとはexe化とGitHubでの公開。