3
9

More than 3 years have passed since last update.

Tkinterで動画を再生する最低限の方法

Posted at

概要

pythonでGUIアプリが作れるtkinterに動画を再生する方法をまとめた記事です
openCVを使うため、音声の出力はないです

完成形は最低限(動画の表示と再生,停止の制御のみ)ですのでこんな感じです
image.png
python 3.8.3
opencv-python 4.4.0.44
pillow 7.2.0

ソース

VideoPlayer.py
import cv2
from PIL import Image, ImageTk
import tkinter as tk
from tkinter import messagebox
import threading as th

lock = th.Lock()

class VideoPlayer(tk.Frame):
    def __init__(self,master=None):
        super().__init__(master,width=1000,height=500)
        master.minsize(width=1000,height=500)
        self.config(bg="#000000")
        self.pack(expand=True,fill=tk.BOTH)
        self.video = None
        self.playing = False
        self.video_thread = None
        self.create_video_button()

    def create_video_button(self):
        self.video_button = tk.Button(
            self,
            width = 100,
            height = 25,
            bg="#DDDDDD",
            relief="flat",
            command = self.push_play_button,
        )
        self.video_button.pack(expand=True,fill=tk.BOTH)

    def get_video(self,path):
        self.video = cv2.VideoCapture(path)

    def push_play_button(self):
        if self.video == None:
            messagebox.showerror('エラー','動画データがありません')
            return

        self.playing = not self.playing
        if self.playing:
            self.video_thread = th.Thread(target=self.video_frame_timer)
            self.video_thread.setDaemon(True)
            self.video_thread.start()            
        else:
            self.video_thread = None 

    def next_frame(self):
        global lock
        lock.acquire()
        ret, self.frame = self.video.read()
        if not ret:
            messagebox.showerror("エラー","次のフレームがないので停止します")
            self.playing = False
        else:
            rgb = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
            pil = Image.fromarray(rgb)
            x = self.video_button.winfo_width()/pil.width
            y = self.video_button.winfo_height()/pil.height
            ratio = x if x<y else y #三項演算子 xとyを比較して小さい方を代入
            pil = pil.resize((int(ratio*pil.width),int(ratio*pil.height)))
            image = ImageTk.PhotoImage(pil)
            self.video_button.config(image=image)
            self.video_button.image = image
        lock.release()

    def video_frame_timer(self):
        while self.playing:
            self.next_frame()

if __name__ == "__main__":
    root = tk.Tk()
    path = "*.mp4"
    app = VideoPlayer(master=root)
    app.get_video(path)
    root.mainloop()

画面をクリックするだけで、動画が再生される

なぜthreadなのか

tkinterにはafter()と呼ばれる、数秒間隔で実行してくれる関数がありますが、
問題点が複数あります

  • 約数秒間隔
  • 重い処理が挟まると再生も遅くなる

実はafter()は正確な秒数間隔で実行が行われていないみたいです
また、メインの中で重い処理を行うと、伴って動画の再生もスローになってしまいます

その点threadを使うことで、動画の再生はサブプロセスで動き、メインの処理の負荷を無視できます

主に関係のあるソース

thread
 def push_play_button(self):
        self.playing = not self.playing
        if self.playing:
            self.video_thread = th.Thread(target=self.video_frame_timer)
            self.video_thread.setDaemon(True)
            self.video_thread.start()            
        else:
            self.video_thread = None 

 def video_frame_timer(self):
        while self.playing:
            self.next_frame()

画面をクリックしたときthreadを作成し、
画面の更新はwhileで実行している
threadはdaemonにすることで、画面を閉じたときに勝手に終了してくれる
threadは上書きされるため、threadは常にメインとサブの2つのみ

canvasを使わない理由

先駆者様は動画を再生するためにcanvasを使っていますが、
処理が重いため、画面が白くちらつきました(処理が描画に間に合ってない)

labelやbuttonを用意して、イメージを更新することで同じことができ、
処理も軽いのでよっぽどの理由がない限りcanvasで作る必要はないと思います
(buttonで作ると再生ボタンも兼ねられるので個人的に好き)

まとめ

tkinter cpenCVで動画処理の記事はいっぱいありますが、
threadを使って動画データを処理する記事が思った以上になかったです
拙い記事ですが、ないよりましの精神で書きましたので、少しでも参考になれば幸いです

3
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
9