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

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

新規登録して質問してみよう
ただいま回答率
85.47%
OpenCV

OpenCV(オープンソースコンピュータービジョン)は、1999年にインテルが開発・公開したオープンソースのコンピュータビジョン向けのクロスプラットフォームライブラリです。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

1回答

1867閲覧

Tkinterでthreadingを使用するとOpenCVのウィンドウが閉じてしまう

nkfrom_asu

総合スコア10

OpenCV

OpenCV(オープンソースコンピュータービジョン)は、1999年にインテルが開発・公開したオープンソースのコンピュータビジョン向けのクロスプラットフォームライブラリです。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2022/03/10 02:22

編集2022/03/22 05:52

前提・実現したいこと

Tkinterでサイコロを振るアプリを作成しています。
ボタンを押すと乱数を生成し、それに対応したGIFが流れるというものです。
サイコロが転がり、目が決定するという、ループなしで1秒のGIFを6つ用意しています。
ボタンの上にあるEntryから回数を指定します (指定しない場合は1回) 。
GIFの表示にはOpenCVを使用しています。

発生している問題

等確率かどうかを調べるため、合計回数と平均をTkinterのウィンドウに表示しました。
そこで発生した、GIFの表示が終わるまで更新されない問題を解決するため、threadingを使用したのですが、1度実行するとOpenCVのウィンドウが閉じてしまいます。

該当のソースコード

Python

1import tkinter 2import cv2 3import random 4import threading 5 6rng_sum, qty = 0, 0 7fnt = ("Helvetica", 10) 8 9def animated_gif(): 10 rng = random.randint(1, 6) #乱数の生成 11 12 gif = cv2.VideoCapture("1~6/{}.gif".format(rng)) #GIFファイルの読み込み 13 fps = gif.get(cv2.CAP_PROP_FPS) 14 15 images = [] #画像をリストに追加 16 a = 0 17 while True: 18 is_success, img = gif.read() 19 if not is_success: 20 break 21 images.append(img) 22 a += 1 23 24 cv2.namedWindow("OpenCV", cv2.WINDOW_AUTOSIZE) #ウィンドウに表示 25 for b in range(len(images)): 26 cv2.imshow("OpenCV", images[b]) 27 cv2.waitKey(int(1000/fps)) 28 cv2.moveWindow('OpenCV', 100, 100) 29 30 global rng_sum, qty #平均を求める 31 rng_sum += rng 32 qty += 1 33 avg = rng_sum / qty 34 la1 = tkinter.Label(root, text = "total number of times: {}".format(qty), 35 fg = "white", bg = "green", font = fnt) 36 la1.place(x = 0, y = 45) #今は config() で更新しています 37 la2 = tkinter.Label(root, text = "average: {:.2f}".format(avg), 38 fg = "white", bg = "green", font = fnt) 39 la2.place(x = 0, y = 62) #同上 40 41def bu_on(): 42 thread1 = threading.Thread(target = work1) 43 thread1.start() 44 45def work1(): #回数の指定 46 c = en.get() 47 if c == "": 48 animated_gif() 49 else: 50 num = int(c) 51 for d in range(num): 52 animated_gif() 53 54root = tkinter.Tk() 55root.geometry("180x84") #サイズ 56root.geometry("+95+100") #位置 57root.attributes("-topmost", True) #最前面に表示 58root.configure(bg="green") 59en = tkinter.Entry(width = 30) 60en.pack() 61en.bind("<Return>", lambda event: bu_on()) 62bu = tkinter.Button(text = "Roll", width = 30, command = bu_on) 63bu.pack() 64root.mainloop() 65

試したこと

threadingにする前はウィンドウが閉じることはありませんでした。buのcommandをwork1にすると同じ状況になります。

※追記
同じ状況ではありませんでした。buのcommand、またはen.bindのlambdaをwork1にし、work1を1度でも実行すると、その後はbu_onを実行してもウィンドウが閉じることはありませんでした。根本的な解決には至っていないため、引き続き回答よろしくお願いします。

補足情報(FW/ツールのバージョンなど)

Python 3.10.2
初めてのアプリ作成なので見にくい部分もあると思いますが、よろしくお願いします。

追記

Python

1import tkinter 2import random 3 4root = tkinter.Tk() 5photo = tkinter.PhotoImage(file = "1~6/1.gif") 6gif_index = 0 7start_flag = False 8 9def times(t): 10 global start_flag 11 i = int(en.get() or 1) 12 if start_flag and t <= i: 13 roll() 14 root.after(1200, times, t + 1) 15 bu1.config(state = "disabled") 16 else: 17 bu1.config(state = "normal") 18 #なくてもいい? 19 start_flag = False 20 21def start(): 22 global start_flag 23 start_flag = True 24 times(1) 25 26def stop(): 27 global start_flag 28 start_flag = False 29 30def roll(): 31 global photo 32 rng = random.randint(1, 6) 33 photo = tkinter.PhotoImage(file="1~6/{}.gif".format(rng)) 34 #la["image"] = photo 35 la.config(image = photo) 36 gif() 37 38def gif(): 39 global gif_index 40 try: 41 photo.config(format="gif -index {}".format(gif_index)) 42 gif_index += 1 43 except tkinter.TclError: 44 gif_index = 0 45 else: 46 root.after(10, gif) 47 48 #print(gif_index, end = " ") 49 #if gif_index == 0: 50 # print("\n") 51 52la = tkinter.Label(root, image = photo) 53en = tkinter.Entry() 54bu1 = tkinter.Button(root, text = "Roll", command = start) 55bu2 = tkinter.Button(root, text = "Stop", command = stop) 56 57la.pack() 58en.pack() 59bu1.pack() 60bu2.pack() 61 62root.mainloop()

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

マルチスレッドとGUIプログラミング(イベント駆動型プログラミング)の作法で幾つか問題があります。
質問の現象に関わるところでは、

問題の原因: 毎回スレッドを生成している点。
opencv2 の GUI は、バックエンドにもよりますが
最初のウィンドウを生成したスレッドでのみ有効です。

解消法: スレッドを毎回生成するのではなく
「opencv用のスレッド」のスレッドをひとつにして、
2回目以降の呼び出しでも再利用する事で解消できます。
スレッド間の通信には同期Queue (queueモジュール) を使います。

コード、骨格のみ。

python

1 2# スレッドで呼び出す関数 3def worker(queue): 4 # images 読み込み 5 6 cv2.namedwindow(...) 7 for num in iter(queue.get, None): # Queue.put で値を受け取ると動き出す 8 for _ in range(num): 9 cv2.imshow(...) 10 ... 11 12 cv2.deleteAllWindows() 13 14def animated_gif(): 15 num = int(en.get() or 1) 16 queue.put(num) 17 18# cv2 用のスレッド開始 19from queue import Queue 20from threading import Thread 21queue = Queue() 22thread = Thread(target=worker, args=(queue,)) 23thread.start() 24 25 26# スレッドの後始末 27 28root.mainloop() # の後、ウィンドウが閉じられた後 29queue.put(None) 30thread.join()

他の問題点
別スレッドからスレッドを跨ぐリソースのアクセスは安全ではありません。
質問のコードでは、別スレッドからの tkinter の直接操作が該当。
ここも、別スレッド側からは Queue を用いて通知を行い、(main -> sub と sub -> main の2つ必要)
メインスレッド側ではタイマー(tkinterではafter関数) で定期的に読み出す処理を行います。


関連: Python(TkInter) 動作があるGIFアニメーションを表示したい
opencv は使わずに tkinter でGIFアニメーションを表示。
tkinter ではアニメーションGIFはサポートされてないのですが、gif画像のフレーム毎の読み出しは可能なので、
スレッドではなく、tkinterのタイマー機能でアニメーションを実装します。

投稿2022/03/12 06:04

編集2022/03/12 07:58
teamikl

総合スコア8664

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nkfrom_asu

2022/03/17 08:40

回答ありがとうございます。返信が遅くなり申し訳ありません。 PhotoImageでのGIFの表示は以前に拝見し試しましたが、色抜けしてしまいました。 ただ、再び確認してみたところ、色抜けしない方法を見つけました。 今回は動画をGIFに変換して使用しているのですが、書き出し方法で違いがありました。 PhotoshopやEZGIF(https://ezgif.com/)で書き出すと色抜けが発生し、 Premiere ProやCreative Cloud Express(https://www.adobe.com/jp/express/feature/video/video-to-gif)で書き出すと発生しませんでした。 色抜け問題は解決したので、書き直しているものを本文に追記しました。 これからマルチスレッドにしていきますが、この時点で修正したほうが良い部分がありましたらアドバイスいただけると嬉しいです。
teamikl

2022/03/17 11:01 編集

GIFの詳細については、 再生速度に緩急があるようなデータだと手直しが必要かもしれません。 imshow~等は使わずにtkinter に描画する場合は、マルチスレッドは不要で tkinter のタイマー機能(after関数) で処理を完結出来ます。 選択肢としては、opencv2 で読み出して、imshow~等は使わずに、 Pillow(PIL)のPhotoImageを使いtkinter で描画する組み合わせもあります。 cv2.imshow 等のcv2のGUIは、GUIのバックエンド次第で挙動が変わってくるので、 他のGUIライブラリと併用する場合は、使わない方が良いです。 ==== マルチスレッドのマルチスレッド・プログラミングで気を付ける事。 - サブスレッドからは GUI (tkinter) 関連のリソースにアクセスしない。 - GUI 関連の操作は必ずメインスレッドで行う。 スレッドを跨ぐリソースのアクセスは基本禁止です。 質問のコードで言うと、メインスレッドで利用しているリソース tkinter に対し 別スレッドで実行されるanimated_gif内からの tkinter の Label 生成が NG です。 例外は logging や queue 等のモジュールは、マルチスレッドで使っても安全な設計になってます。 実装方法: サブスレッドからGUIを更新したい時、代わりに - サブスレッドからは同期Queue へデータを put する事でメインスレッド通知 - メインスレッド側では、tkinter のタイマー機能(after関数) で定期的に Queue のデータを確認  Queueの内容を読み出しデータに応じた処理をする。  Queue の読み出しでは、デッドロックにならないように注意。  GUIのスレッド(メインスレッド)でブロッキング処理が発生すると、GUIがフリーズします。  A) queue.get で読み出す場合は、事前にqueue のサイズを確認してから読み出す  B) queue.get_nowait で読み出して、queue が空の時の例外処理をする 具体的な実装コードは、tkinter で動画再生するサンプルコードが参考になると思います。
nkfrom_asu

2022/03/19 07:35

afterで済むならそのほうがいいかもしれないですね。 自分の能力ではまだthreadingを十分に活かせない気がします… 今後使用する機会があれば参考にさせていただきます。 https://daeudaeu.com/mainloop/#sleep-after こちらのサイトを参考にafterを使用し、追記のコードを更新しました。 これで問題がなければ、この質問を締めたいと思います。 丁寧に回答してくださり、ありがとうございます。
teamikl

2022/03/20 03:13

汎用ライブラリとしては、アニメーション速度の制御が課題としてありますが、 今回の目的が特定ファイルの再生で アニメーションの速度を決め打ちでも良いのであれば、問題ありません。 対策: Pillow(PIL) を使い、フレーム毎の duration 情報を所得する 問題になる事例: 追記のコードではroll が 1.2秒 gif が 0.01 秒なので、 フレーム数が 120 以上あると、再生時間は1.2秒以上となり 前のアニメーションが残っていて gif 関数が2重に適応されます。 対策 => 最初のファイルの再生が終わってから、次のファイルを再生する 簡単なアニメーション用途であれば、 tkinterのタイマー機能(after関数)での実装が適切です。 スレッドを使う場合でも GUI への描画関連は必ずメインスレッド側で行う必要があり、 定期的に呼びだすスレッドからの通知・キューの読み出しに after を使うことになります。 #なくてもいい? start_flag = False スタート時に初期化するので、ここはなくても期待通りに動きますが、 デバッグ等で確認する時の為にあったほうがよいです。 Roll を押してからアニメーション再生途中に入力数値を変更した場合等、 他にも課題はありますが、本題のGIFアニメーションが解決してるなら、大丈夫だと思います。
nkfrom_asu

2022/03/20 07:44

1秒の動画をGIFにしたので、余裕をもって1.2秒に指定したのですが、 処理時間と画像数を確認してみると、処理時間は1枚につき0.0156秒(たまに0.0312秒)、 画像数は25枚だったので、実際には0.5秒弱でした(元の動画より速いことに今気づいた)。 ただ、0.5秒では処理が追い付いておらず、1秒でも違和感はないので、1秒で進めようと思います。 最後のご指摘は現在、他の機能(停止ボタンを一時停止ボタンに変更、リセットボタンの追加)を 追加するのと同時に、stateで制御することにより解決しています。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問