質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.32%

Tkinterのウィンドウに画像認識の結果を渡したいが、フリーズしてしまう

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 501

falcon_titan

score 14

ウィンドウのフリーズ
現在画像認識のプログラムを作成しており、認識自体はできるようになりました。
実行の結果をTkinterで文字として表示しようと思ったのですが、動作が固まってしまいます。
Tkinterのウィンドウを消せばまた認識をしてくれるのですが、止まることなく実行するにはどうすればいいでしょうか。
解決策を教えていただけると嬉しいです。

画像認識について
認識は、あらかじめ作成したデータを読み込んで使用しています。
写真はcv2で撮影したリアルタイム映像から、静止画として同じ階層に一時的に保存し、
PILで読み込んでいます。

環境
python 3.6.1
opencv-contrib-python 4.2.0.32
opencv-python  4.2.0.32
pillow  4.1.1
scikit-learn 0.18.1

コードです↓

import cv2
import os
import datetime
import pickle
from PIL import Image
from sklearn import svm
import numpy as np
import tkinter as tk 
from tkinter import font

# ウィンドウ表示の定義
def print_text(result):
    root = tk.Tk()#ウィンドウを作る

    root.title("ティッシュ認識システム") #タイトルの変更
    root.geometry("300x100") #サイズの指定

    font1 = font.Font(family='Helvetica', size=20, weight='bold') #フォントを設定

    text = tk.StringVar()#textをStringVar関数にする

    if result ==1:
        text.set("これはティッシュです")

    else:
        text.set("これは床です")

    label = tk.Label(textvariable = text, font = font1) #ラベルを追加
    label.pack()

    root.mainloop() #実行して表示


# 画像認識の学習データを読み込む
filename = 'tissue_model_1.sav'
clf = pickle.load(open(filename, 'rb'))

# 変数の定義
size = 16

# 認識システムを関数で定義
def save_frame_camera_cycle(device_num, dir_path, basename, cycle, ext='JPG', delay=1, window_name='frame'):
    cap = cv2.VideoCapture(1)# カメラは1(usbカメラ)を用いる

    if not cap.isOpened():#カメラが見つからなかった場合
        return#中止する 

    os.makedirs(dir_path, exist_ok=True)
    base_path = os.path.join(dir_path, basename)

    n = 0
    while True: #Trueでずっとループさせる
        ret, frame = cap.read()
        cv2.imshow(window_name, frame)
        if cv2.waitKey(delay) & 0xFF == ord('q'):# qが押されたら終了する
            break
        if n == cycle:#cycleとnが同じ数になったら写真を保存する

            cv2.imwrite('{}_{}.{}'.format(base_path, datetime.datetime.now().strftime('%Y%m%d%H%M%S%f'), ext), frame)#画像を保存(学習用に保存)
            cv2.imwrite('picture.' + ext, frame)#認識用の画像を保存
            img = Image.open('picture.JPG')#画像を開く
            img = img.resize((size,size))#リサイズ
            ary = np.array(img).reshape(-1,)#一次元の配列にする
            result = clf.predict(ary)#認識
            print(result)#結果を表示

            print_text(result = result)

            n = 0
        n += 1

    cv2.destroyWindow(window_name)

#関数を実行
save_frame_camera_cycle(0, 'camera_data', 'camera_capture_cycle', 15)

アドバイスを受け、マルチスレッドで実行するようにプログラムを変更しました。

import cv2
import os
import datetime
from sklearn import svm
import pickle
from PIL import Image
import numpy as np
import tkinter as tk 
from tkinter import font
import threading

# ウィンドウ表示の定義
def print_text():
    root = tk.Tk()#ウィンドウを作る

    root.title("ティッシュ認識システム") #タイトルの変更
    root.geometry("300x100") #サイズの指定

    font1 = font.Font(family='Helvetica', size=20, weight='bold') #フォントを設定

    text = tk.StringVar()#textをStringVar関数にする

    if result ==1:
        text.set("これはティッシュです")

    else:
        text.set("これは床です")

    label = tk.Label(textvariable = text, font = font1) #ラベルを追加
    label.pack()

    root.mainloop() #実行して表示
    print("a")

# 画像認識の学習データを読み込む
filename = 'tissue_model_1.sav'
clf = pickle.load(open(filename, 'rb'))

# 変数の定義
size = 16

# 認識システムを関数で定義
def save_frame_camera_cycle(device_num, dir_path, basename, cycle, ext='JPG', delay=1, window_name='frame'):
    cap = cv2.VideoCapture(1)# カメラは1(usbカメラ)を用いる

    if not cap.isOpened():#カメラが見つからなかった場合
        return#中止する 

    os.makedirs(dir_path, exist_ok=True)
    base_path = os.path.join(dir_path, basename)

    n = 0
    while True: #Trueでずっとループさせる
        ret, frame = cap.read()
        cv2.imshow(window_name, frame)
        if cv2.waitKey(delay) & 0xFF == ord('q'):# qが押されたら終了する
            break
        if n == cycle:#cycleとnが同じ数になったら写真を保存する

            cv2.imwrite('{}_{}.{}'.format(base_path, datetime.datetime.now().strftime('%Y%m%d%H%M%S%f'), ext), frame)#画像を保存
            cv2.imwrite('picture.' + ext, frame)#画像を1時的に保存
            img = Image.open('picture.JPG')#画像を開く
            img = img.resize((size,size))#リサイズ
            ary = np.array(img).reshape(-1,)#一次元の配列にする
            global result
            result = clf.predict(ary)#認識
            print(result)#結果を表示

            print_text(result = result)

            n = 0
        n += 1

    cv2.destroyWindow(window_name)

#関数を実行
t1 = threading.Thread(target = save_frame_camera_cycle, args = (0, 'camera_data', 'camera_capture_cycle', 15))
t2 = threading.Thread(target = print_text,)
t2.start()
t1.start()


しかし、

Exception in thread Thread-12:
Traceback (most recent call last):
  File "C:\Users\ユーザー名\Anaconda3\lib\threading.py", line 916, in _bootstrap_inner
    self.run()
  File "C:\Users\ユーザー名\Anaconda3\lib\threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "<ipython-input-8-c90b360b3e2d>", line 19, in print_text
    font1 = font.Font(family='Helvetica', size=20, weight='bold') #フォントを設定
  File "C:\Users\ユーザー名\Anaconda3\lib\tkinter\font.py", line 93, in __init__
    tk.call("font", "create", self.name, *font)
RuntimeError: main thread is not in main loop

C:\Users\ユーザー名\Anaconda3\lib\site-packages\sklearn\utils\validation.py:395: DeprecationWarning: Passing 1d arrays as data is deprecated in 0.17 and will raise ValueError in 0.19. Reshape your data either using X.reshape(-1, 1) if your data has a single feature or X.reshape(1, -1) if it contains a single sample.
  DeprecationWarning)
Exception in thread Thread-11:
Traceback (most recent call last):
  File "C:\Users\ユーザー名\Anaconda3\lib\threading.py", line 916, in _bootstrap_inner
    self.run()
  File "C:\Users\ユーザー名\Anaconda3\lib\threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "<ipython-input-8-c90b360b3e2d>", line 69, in save_frame_camera_cycle
    print_text(result = result)
TypeError: print_text() got an unexpected keyword argument 'result'


と、結果となるresultをTkinterウィンドウの関数が受け取ることができませんでした。
変数を共有するには、どうしたらよいでしょうか?

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • t_obara

    2020/03/19 19:26

    GUIプログラムでは基本的にUIの反応をmainloopで行なっているためmainloop以外のループを使用してはいけません。その場合には、マルチスレッド、マルチプロセス、タイマーなどを利用する必要があります。

    キャンセル

  • falcon_titan

    2020/03/20 17:57

    分かりました。マルチスレッドを利用してみます。アドバイスありがとうございます。

    キャンセル

  • falcon_titan

    2020/03/20 18:15

    https://python.civic-apps.com/threading/
    を参考に、threadingを用いてマルチスレッドにしてみたのですがうまくいきませんでした。
    変数を共有させるには、どうしたらよいでしょうか?

    キャンセル

回答 1

checkベストアンサー

+1

TypeError: print_text() got an unexpected keyword argument 'result'

t2 のスレッドとは別に、t1 スレッドの
save_frame_camera_cycle関数内から print_text(result = result)
のように呼び出されていますが、
関数print_text()は引数を取るように設計されていません。

RuntimeError: main thread is not in main loop

これは、全く同じエラーは再現できなかったのですが
tkinterのウィンドウを閉じた後のものではないでしょうか?

ウィンドウを閉じられた後に、別スレッドからtkinterを操作しようとして
実行時エラーになっているものだと思われます。
(エラーの行自体の問題ではなく、終了時のタイミング次第)

改善案:

- tkinterは普通にメインスレッドで実行するようにします。
- save_frame_camera_cycleのスレッドをデーモン化する(daemon=True)
  - これにより、tkinterのウィンドウが閉じられた時(メインスレッドが終了した時)
    サブスレッドも一緒に終了するようになるため、
    サブスレッドが終了後のtkinterにアクセスすることを(ある程度)防げます。

スレッド→Tkinterへの値の受け渡しに関して、

スレッド内から別スレッドで作った GUI の描画を直接行うのは
スレッドセーフな操作ではないので、
GUIライブラリが用意しているキューを経由して行います。
tkinterの場合はroot.after()を使います。

スレッドで演算した結果をtkinterに表示するデモ

import time
import threading
import tkinter as tk

# ウィンドウ表示の定義
def create_result_viewer():
    root = tk.Tk()
    label = tk.Label(root)
    label.pack()
    return root, label

# 認識システムを関数で定義
def save_frame_camera_cycle(cycle, updateResult, dummyResultList):
    while True:
        for _ in range(cycle):
            time.sleep(1)
        else:
            updateResult(next(dummyResultList))

if __name__ == '__main__':
    # resultをシミュレートするダミーの結果リスト
    import itertools
    dummyResultList = itertools.cycle([1, 0, 1, 1, 0, 0, 0, 1])

    root, label = create_result_viewer()

    def showResult(result):
        if result == 1:
            label["text"] = "これはティッシュです"
        else:
            label["text"] = "これは床です"

    def updateResult(result):
        root.after(100, lambda: showResult(result))

    # NOTE: この小さなサンプルプログラムでは、updateResult の代わりに showResult を引数に渡しても動作しますが、
    # 複雑になってくると破綻するので、root.after 経由で実行するほうが安全。(スレッドセーフ)
    t1 = threading.Thread(target=save_frame_camera_cycle, args=(5, updateResult, dummyResultList), daemon=True)
    t1.start()
    root.mainloop()

課題: cv2.imshow, cv2.waitKey はスレッド内では期待通りに動作しません
cv2.imshow でウィンドウが表示されますがフリーズ状態になります。
(上記サンプルコードからは意図的に省きました)

cv2.imshowとtkinterを同時に使う場合は、
双方をメインスレッドで動かすことになるので、
プロセスを別ける(マルチプロセス)事になると思います。

マルチスレッドで行う場合、tkinterを用いてるので、
imshowの代わりにtkitnerのキャンバスに描画するのが良いでしょう。

cv2.waitKey の入力待ちは、tkinterでキー入力を受けてキューに入れ、
スレッド側で非同期で読み出すことで実現できます。

以下の質問の回答で、tkinter+opencvでの
マルチプロセスやマルチスレッドでのキューを用いた例がありました。

関連

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.32%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る