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

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

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

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

Tkinter

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

Python

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

Q&A

解決済

1回答

1097閲覧

tkinterの動画表示がうまくいかない

daikooooooon

総合スコア9

OpenCV

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

Tkinter

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

Python

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

0グッド

1クリップ

投稿2021/09/22 14:36

このサイトのコードをもとに、オリジナルのGUIアプリを作成しようとしています。

しかし、動画を最初に選択し、もう一度動画を選択すると挙動がおかしいです。

具体的には、ボタンをクリックすると動画が再生されるはずなのですが、
ボタンをクリックすると、内部ではフレームが進み、
もう一度クリックすると、止めたところのフレーム画像が表示されます。

mainloop等などについて自分でも調べたのですが、原因がわかりません。

わかる方がおられましたら、教えていただけませんでしょうか。

コード

Python

1class Model(): 2 3 def __init__(self): 4 5 # 動画オブジェクト参照用 6 self.video = None 7 8 # 画像処理の設定 9 self.gray = False 10 self.flip= False 11 12 # 読み込んだフレーム 13 self.frames = None 14 15 # PIL画像オブジェクト参照用 16 self.image = None 17 18 # Tkinter画像オブジェクト参照用 19 self.image_tk = None 20 21 22 def create_video(self, path): 23 '動画オブジェクトの生成を行う' 24 25 # pathの動画から動画オブジェクト生成 26 self.video = cv2.VideoCapture(path) 27 28 def advance_frame(self): 29 'フレームを読み込んで1フレーム進める' 30 31 if not self.video: 32 return 33 34 # フレームの読み込み 35 ret, self.frame = self.video.read() 36 37 return ret 38 39 def reverse_video(self): 40 '動画を先頭に戻す' 41 42 self.video.set(cv2.CAP_PROP_POS_FRAMES, 0) 43 44 def create_image(self, size): 45 'フレームの画像を作成' 46 47 t1 = time.time() 48 49 # フレームを読み込み 50 frame = self.frame 51 if frame is None: 52 print("None") 53 54 # 設定に応じて画像処理 55 if self.gray: 56 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 57 if self.flip: 58 frame = cv2.flip(frame, 1) 59 60 # PIL イメージに変換 61 rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 62 pil_image = Image.fromarray(rgb_frame) 63 64 # 指定サイズに合わせて画像をリサイズ 65 66 # 拡大率を計算 67 ratio_x = size[0] / pil_image.width 68 ratio_y = size[1] / pil_image.height 69 70 if ratio_x < ratio_y: 71 ratio = ratio_x 72 else: 73 ratio = ratio_y 74 75 # リサイズ 76 self.image = pil_image.resize( 77 ( 78 int(ratio * pil_image.width), 79 int(ratio * pil_image.height) 80 ) 81 ) 82 t2 = time.time() 83 84 print(f"経過時間:{t2-t1}") 85 86 def get_image(self): 87 'Tkinter画像オブジェクトを取得する' 88 89 if self.image is not None: 90 # Tkinter画像オブジェクトに変換 91 self.image_tk = ImageTk.PhotoImage(self.image) 92 return self.image_tk 93 94 def get_fps(self): 95 '動画のFPSを取得する' 96 97 if self.video is None: 98 return None 99 100 return self.video.get(cv2.CAP_PROP_FPS) 101 102 def set_gray(self): 103 self.gray = not self.gray 104 105 def set_flip(self): 106 self.flip = not self.flip 107 108class View(): 109 110 def __init__(self, app, model): 111 112 self.master = app 113 self.model = model 114 115 # アプリ内のウィジェットを作成 116 self.create_widgets() 117 118 def create_widgets(self): 119 'アプリ内にウィジェットを作成・配置する' 120 121 # キャンバスのサイズ 122 canvas_width = 500 123 canvas_height = 300 124 125 # キャンバスとボタンを配置するフレームの作成と配置 126 self.main_frame = tkinter.Frame( 127 self.master 128 ) 129 self.main_frame.pack() 130 131 # キャンバスを配置するフレームの作成と配置 132 self.canvas_frame = tkinter.Frame( 133 self.main_frame 134 ) 135 self.canvas_frame.grid(column=1, row=1) 136 137 # ユーザ操作用フレームの作成と配置 138 self.operation_frame = tkinter.Frame( 139 self.main_frame 140 ) 141 self.operation_frame.grid(column=2, row=1) 142 143 144 # キャンバスの作成と配置 145 self.canvas = tkinter.Canvas( 146 self.canvas_frame, 147 width=canvas_width, 148 height=canvas_height, 149 bg="#EEEEEE", 150 ) 151 self.canvas.pack() 152 153 # ファイル読み込みボタンの作成と配置 154 self.load_button = tkinter.Button( 155 self.operation_frame, 156 text="動画選択" 157 ) 158 self.load_button.pack() 159 160 # グレーON/OFFボタンの作成と配置 161 self.gray_button = tkinter.Button( 162 self.operation_frame, 163 text="モノクロON/OFF" 164 ) 165 self.gray_button.pack() 166 167 # フリップ/OFFボタンの作成と配置 168 self.flip_button = tkinter.Button( 169 self.operation_frame, 170 text="フリップON/OFF" 171 ) 172 self.flip_button.pack() 173 174 175 def draw_image(self): 176 '画像をキャンバスに描画' 177 178 image = self.model.get_image() 179 180 if image is not None: 181 # キャンバス上の画像の左上座標を決定 182 sx = (self.canvas.winfo_width() - image.width()) // 2 183 sy = (self.canvas.winfo_height() - image.height()) // 2 184 185 # キャンバスに描画済みの画像を削除 186 objs = self.canvas.find_withtag("image") 187 for obj in objs: 188 self.canvas.delete(obj) 189 190 # 画像をキャンバスの真ん中に描画 191 self.canvas.create_image( 192 sx, sy, 193 image=image, 194 anchor=tkinter.NW, 195 tag="image" 196 ) 197 198 def select_open_file(self, file_types): 199 'オープンするファイル選択画面を表示' 200 201 # ファイル選択ダイアログを表示 202 file_path = tkinter.filedialog.askopenfilename( 203 initialdir=".", 204 filetypes=file_types 205 ) 206 return file_path 207 208 def draw_play_button(self): 209 '再生ボタンを描画' 210 211 # キャンバスのサイズ取得 212 width = self.canvas.winfo_width() 213 height = self.canvas.winfo_height() 214 215 # 円の直径を決定 216 if width > height: 217 diameter = height 218 else: 219 diameter = width 220 221 # 端からの距離を計算 222 distance = diameter / 10 223 224 # 円の線の太さを計算 225 thickness = distance 226 227 # 円の描画位置を決定 228 sx = (width - diameter) // 2 + distance 229 sy = (height - diameter) // 2 + distance 230 ex = width - (width - diameter) // 2 - distance 231 ey = height - (height - diameter) // 2 - distance 232 233 # 丸を描画 234 self.canvas.create_oval( 235 sx, sy, 236 ex, ey, 237 outline="white", 238 width=thickness, 239 tag="oval" 240 ) 241 242 # 頂点座標を計算 243 x1 = sx + distance * 3 244 y1 = sy + distance * 2 245 x2 = sx + distance * 3 246 y2 = ey - distance * 2 247 x3 = ex - distance * 2 248 y3 = height // 2 249 250 # 三角を描画 251 self.canvas.create_polygon( 252 x1, y1, 253 x2, y2, 254 x3, y3, 255 fill="white", 256 tag="triangle" 257 ) 258 259 def delete_play_button(self): 260 self.canvas.delete("oval") 261 self.canvas.delete("triangle") 262 263class Controller(): 264 265 def __init__(self, app, model, view): 266 self.master = app 267 self.model = model 268 self.view = view 269 270 271 # 動画再生中かどうかの管理 272 self.playing = False 273 274 # フレーム進行する間隔 275 self.frame_timer = 0 276 277 # 描画する間隔 278 self.draw_timer = 50 279 280 self.set_events() 281 282 def set_events(self): 283 '受け付けるイベントを設定する' 284 285 # キャンバス上のマウス押し下げ開始イベント受付 286 self.view.canvas.bind( 287 "<ButtonRelease-1>", 288 self.button_press 289 ) 290 291 # 動画選択ボタン押し下げイベント受付 292 self.view.load_button['command'] = self.push_load_button 293 294 # モノクロON/OFFボタン押し下げイベント受付 295 self.view.gray_button['command'] = self.push_gray_button 296 297 # フリップON/OFFボタン押し下げイベント受付 298 self.view.flip_button['command'] = self.push_flip_button 299 300 def draw(self): 301 '一定間隔で画像等を描画' 302 303 # 再度タイマー設定 304 self.master.after(self.draw_timer, self.draw) 305 306 # 動画再生中の場合 307 if self.playing: 308 # フレームの画像を作成 309 self.model.create_image( 310 ( 311 self.view.canvas.winfo_width(), 312 self.view.canvas.winfo_height() 313 ) 314 ) 315 316 # 動画1フレーム分をキャンバスに描画 317 self.view.draw_image() 318 319 def frame(self): 320 '一定間隔でフレームを進める' 321 322 # 再度タイマー設定 323 self.master.after(self.frame_timer, self.frame) 324 325 # 動画再生中の場合 326 if self.playing: 327 # 動画を1フレーム進める 328 ret = self.model.advance_frame() 329 330 # フレームが進められない場合 331 if not ret: 332 # フレームを最初に戻す 333 self.model.reverse_video() 334 self.model.advance_frame() 335 336 337 def push_load_button(self): 338 '動画選択ボタンが押された時の処理' 339 340 file_types = [ 341 ("WMVファイル", "*.wmv"), 342 ("MP4ファイル", "*.mp4"), 343 ] 344 345 # ファイル選択画面表示 346 file_path = self.view.select_open_file(file_types) 347 348 if len(file_path) != 0: 349 350 # 動画オブジェクト生成 351 self.model.create_video(file_path) 352 353 # 最初のフレームを表示 354 self.model.advance_frame() 355 self.model.create_image( 356 ( 357 self.view.canvas.winfo_width(), 358 self.view.canvas.winfo_height() 359 ) 360 ) 361 self.model.reverse_video() 362 self.view.draw_image() 363 364 # 再生ボタンの表示 365 self.view.delete_play_button() 366 self.view.draw_play_button() 367 368 # FPSに合わせてフレームを進める間隔を決定 369 fps = self.model.get_fps() 370 self.frame_timer = int(1 / fps * 1000 + 0.5) 371 372 # フレーム進行用のタイマースタート 373 self.master.after(self.frame_timer, self.frame) 374 375 # 画像の描画用のタイマーセット 376 self.master.after(self.draw_timer, self.draw) 377 378 def button_press(self, event): 379 'マウスボタン押された時の処理' 380 381 # 動画の再生/停止を切り替える 382 if not self.playing: 383 self.playing = True 384 385 # 再生ボタンの削除 386 self.view.delete_play_button() 387 else: 388 self.playing = False 389 390 # 再生ボタンの描画 391 self.view.draw_play_button() 392 393 def push_gray_button(self): 394 self.model.set_gray() 395 396 def push_flip_button(self): 397 self.model.set_flip() 398 399 400 401app = tkinter.Tk() 402 403app.title("動画再生アプリ") 404 405model = Model() 406view = View(app, model) 407controller = Controller(app, model, view) 408 409app.mainloop()

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

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

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

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

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

Bull

2021/09/25 07:03 編集

当方の環境でソースコードをコピペして実行してみましたが、経過時間の表示の負荷が大きくて描画が間に合っていないのかもしれません。 「print(f"経過時間:{t2-t1}")」をコメントにしてみては如何でしょうか? あるいは、描画する間隔 (draw_timer) をもう少し大きくしてみてもいいかもしれません。 あまり大きくするとスローモーションになってしまいますが。 当方の環境 (i7-6500U) ですと print(~) をコメントにすると、draw_timer = 30 位にしても、正常に再生できました。
daikooooooon

2021/09/25 07:24 編集

Bullさん 検証していただきありがとうございます すいません私の説明不足だったのですが 最初に選択した動画は問題なく再生できます しかし、例えば動画A, Bがあるとき まず動画Aを選択して再生し、 その後動画Bを選択して再生すると挙動がおかしくなります 2つ目に再生した動画は、再生後、停止したときしか画面が更新されません アプリを立ち上げて最初に選択した動画しかうまく再生されません printデバッグしたところでは、 内部では、2つ目以降の動画を再生しているときも、通常の速さでフレーム画像が読みこめているので 2つ目以降の動画だけ表示処理に問題がありそうです
daikooooooon

2021/09/25 07:31 編集

-------------------------------------------------------------------------------------------------------------------- def draw_image(self): '画像をキャンバスに描画' image = self.model.get_image() if image is not None: # キャンバス上の画像の左上座標を決定 sx = (self.canvas.winfo_width() - image.width()) // 2 sy = (self.canvas.winfo_height() - image.height()) // 2 # キャンバスに描画済みの画像を削除 objs = self.canvas.find_withtag('image') for obj in objs: self.canvas.delete(obj) # 画像をキャンバスの真ん中に描画 self.canvas.create_image( sx, sy, image=image, anchor=tkinter.NW, tag='image' ) print("aaaaaaaaaaaaaaaaaaaaaaa") ############################ -------------------------------------------------------------------------------------------------------------------- このようにすると、2つ目の動画では、 再生中何もprintされず、停止すると、 進んだフレーム数だけaaa... が表示されます
Bull

2021/09/25 08:42 編集

問題の本質を誤認している可能性もありますが、やはり負荷が高いことにより描画処理が正常にできていない可能性があるのではないかと思います。 と言いますのは、draw_timer の値を変化させると、描画できたり、できなかったりするからです。 処理が追いつかないために、draw 関数が正常に起動できていない可能性もあるのではないかと思われます。 しかし、たとえ負荷が高いことで正常に描画できないとしても、一つ目の動画が正常で二つ目の動画が何故そうなるのかはよくわからないのです。
daikooooooon

2021/09/26 02:55 編集

Bullさんのおかげで原因が判明しました!!! push_load_button()関数中の、self.master.after()が複数回実行されていることが原因でした。 Bullさんのおっしゃるとおり、 self.draw_timer = 30 にすると、1つ目の動画の時点で、2つ目の動画を再生するときと同じ状態になり、 self.draw_timer = 100にすると、2つ目の動画も再生できるが、FPSが速くなっている・今度は3つめの動画から挙動が遅くなる ということが判明し、afterに原因があるのではないかと閃くことができました! 本当に助かりました ありがとうございました
guest

回答1

0

自己解決

Bullさんのおかげで解決しました

push_load_button()関数中の、self.master.after()が複数回実行されないよう
1つ目の動画かを保持するフラグを設定し、1つめの動画のときだけafterを実行することで
解決できました

ありがとうございました

投稿2021/09/26 02:57

daikooooooon

総合スコア9

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問