このページでは Python で「動画を再生するアプリ」を作成する方法およびそのアプリのサンプルスクリプトについて解説していきます。
使用する外部ライブラリ・モジュールは Tkinter・OpenCV2・Pillow(PIL)になります。主に動画を扱うのが OpenCV2 ですので、残念ながら「音声なし」になります。
このページで紹介するサンプルスクリプトは下記環境で動作確認を行っています
- OS:macOS Catalina
- Python:3.8
- Tkinter:8.6
- NumPy:1.18.2
- OpenCV2:4.2.0
- Pillow:7.1.1
作成するアプリ
今回作成するアプリは下のようなものになります。
アプリの画面
アプリの画面は下記のようなウィジェットから構成されます。
スポンサーリンク
アプリの動作
このアプリでは、ボタンを押したりマウス操作により下記を行うことができます。
- 動画を読み込む
- 動画を再生する
- 動画を停止する
- 動画をモノクロ化する
- 動画をフリップする
動画を読み込む
「動画選択」ボタンをクリックすることで、動画ファイル(.MOV と .MP4)の選択を受け付け、選択された動画を読み込みます。
動画を再生する
動画を読み込むと画面左側のキャンバスに動画の最初のフレームおよび再生ボタンが表示されます。
この再生ボタンをクリックすると、動画を再生することができます。
この動画の再生について少し詳細を説明しておきます。
まず前提として、動画は簡単に言うと、複数枚の画像(フレームと言います)を連続して表示することで実現されます。
例えば下記の10枚のフレームから構成される動画があるとしましょう。
動画を読み込むとこの先頭のフレームがキャンバスに描画されます(再生ボタンも)。
さらにキャンバスをクリックすれば、動画が再生されます。この再生は、フレームを一定間隔で次のフレームに切り替えることで実現しています。
この切り替える間隔は、読み込んだ動画の FPS(1秒あたりの表示フレーム数)に基づいて計算していますので、大体読み込んだ動画を他のプレイヤーで再生した時と同じくらいのタイミングでフレームが切り替わると思います(若干ズレます)。
動画を停止する
動画再生中にキャンバスをクリックすると動画がその時点で停止します。
さらに動画が停止した状態でキャンバスをクリックすれば、停止した時点から動画の再生を再開することができます。
動画をモノクロ化する
「モノクロON/OFF」ボタンをクリックすれば、動画がモノクロ化されて表示されるようになります。
これは各フレームを描画する前に、そのフレームをモノクロ化することで実現しています。
動画がモノクロ化された状態で「モノクロON/OFF」ボタンを再度クリックすれば、動画がカラーに戻ります。
動画をフリップする
「フリップON/OFF」ボタンをクリックすれば、動画が左右反転されて表示されるようになります。
これは各フレームを描画する前に、そのフレームを左右反転することで実現しています。
動画が左右反転された状態で「フリップON/OFF」ボタンを再度クリックすれば、動画がさらに左右反転され、元に戻ります。
アプリのクラス設計
ではここからはアプリをどのように開発しているかについて解説していきたいと思います。
各クラスは「Model
」と「View
」と「Controller
」の3つに分かれます。
「Model
」は動画や画像を扱うクラスで、「View
」はアプリの見た目や描画を行うクラスです。「Controller
」はユーザーからのイベント(ボタンクリック・マウス操作など)を受け付け、必要に応じて「Model
」や「View
」に処理の依頼を行うクラスになります。
Model
クラス
Model
クラスは下記の機能を提供します。
- 動画オブジェクトの生成(
create_video
) - フレーム進行(
advance_frame
) - 画像オブジェクトの生成(
create_image
) - 画像オブジェクトの取得(
get_image
) - 動画のFPS取得(
get_fps
) - 動画の巻き戻し(
reverse_video
) - モノクロ設定(
set_gray
) - フリップ設定(
set_flip
)
動画オブジェクトの生成(create_video
)
Model
クラスのオブジェクトは指定されたパスの動画から動画オブジェクトの生成を行います。
具体的には OpenCV2 の VideoCapture
クラスのインスタンスの生成を行います。
self.video = cv2.VideoCapture(path)
フレーム進行(advance_frame
)
Model
クラスのオブジェクトは動画オブジェクトからフレームの読み込みを行います。
# フレームの読み込み
ret, frame = self.video.read()
VideoCapture
クラスの read
メソッドは「次のフレーム」を NumPy 配列として読み込むメソッドです。
動画オブジェクト生成後に read
メソッドを実行した場合、下記のようにフレームの読み取りとフレームの進行が行われていきます。
- 1回目の
read
実行:先頭フレームが読み込まれる - 2回目の
read
実行:先頭フレームから2番目のフレームが読み込まれる - 3回目の
read
実行:先頭フレームから3番目のフレームが読み込まれる - ・・・・
- n回目の
read
実行:先頭フレームからn番目のフレームが読み込まれる
こんな感じで read
メソッドを実行することで、フレームの進行を実現しています。
また読み込んだフレームは次の「画像オブジェクト」の生成で利用します。
画像オブジェクトの生成(create_image
)
Model
クラスのオブジェクトは読み込み済みのフレームに対して PIL 画像オブジェクトの生成および画像処理を行います。
具体的には下記の処理を行います。
- モノクロ処理(モノクロ ON 時のみ)
- フリップ処理(フリップ ON 時のみ)
- PIL 画像オブジェクト生成
- リサイズ
モノクロ処理は OpenCV2 を用いて下記により実行しています。frame
は VideoCapture
クラスの read
メソッドにより読み込まれた NumPy 配列のデータです。
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
またフリップ処理も OpenCV2 を用いて下記により実行しています。ちなみに第2引数を “0
” にすれば上下反転することも可能です。
frame = cv2.flip(frame, 1)
NumPy 配列 frame
からの PIL 画像オブジェクト生成は下記により行っています。
# PIL イメージに変換
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(rgb_frame)
この変換については下記ページで解説していますので、詳しく知りたい方は読んでみてください。
【Python】PIL ⇔ OpenCV2 ⇔ Tkinter 画像オブジェクトの相互変換最後に指定されたサイズ(引数で指定)に合わせて画像のリサイズを行っています(指定されたサイズの矩形に内接に接するように、縦横同アスペクト比でリサイズ)。
主に画像を描画するキャンバスのサイズに合わせてリサイズすることを想定しています。
画像オブジェクトの取得(get_image
)
Model
クラスのオブジェクトが持つ画像オブジェクトを取得する機能も提供しています。
PIL 画像オブジェクトを Tkinter 用のものに変換し、変換後の画像オブジェクトを返却します。
動画のFPS取得(get_fps
)
Model
クラスのオブジェクトが持つ動画オブジェクトに対する動画の FPS を取得するための機能です。
FPSは VideoCapture
クラスの set
メソッドを下記のように実行することで取得することができます(video
は VideoCapture
クラスのインスタンス)。
video.get(cv2.CAP_PROP_FPS)
動画の巻き戻し(reverse_video
)
Model
クラスのオブジェクトが持つ動画オブジェクトに対する動画を先頭に巻き戻す機能です。
VideoCapture
クラスの set
メソッドを下記のように実行することで、動画を先頭に巻き戻すことができます(video
は VideoCapture
クラスのインスタンス)。
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
モノクロ設定(set_gray
)
モノクロの ON/OFF を切り替えるための機能です。
この設定に応じて画像オブジェクトの生成(create_image
)でモノクロ処理を行うかどうかが変わります。
フリップ設定(set_flip
)
フリップの ON/OFF を切り替えるための機能です。
この設定に応じて画像オブジェクトの生成(create_image
)でフリップ処理を行うかどうかが変わります。
スポンサーリンク
View
クラス
View
クラスは主に下記の機能を提供します。
- ウィジェットの作成と配置(
create_widgets
) - 画像の描画(
draw_image
) - 再生ボタンの描画(
draw_play_button
) - 再生ボタンの削除(
delete_play_button
) - ファイル選択画面表示(
select_open_file
)
ウィジェットの作成と配置(create_widgets
)
まず View
クラスのオブジェクトはウィジェットの作成と配置を行います。
具体的には下図のようにウィジェットの配置を行なっています。
画像の描画
アプリ画面のキャンバスに対して画像の描画も行います。こちらは下記ページで解説していますのでこちらをご参照ください。
【Python】画像のトリミング(クロップ・切り取り)アプリを作ってみる【GUI】再生ボタンの描画(draw_play_button
)
アプリ画面のキャンバスに対して、再生ボタンの描画も行っています。
単純に丸と三角を、サイズや配置を調整して再生ボタンっぽく見せているだけです。
丸も三角も Tkinter の Canvas
クラスのメソッドを利用して簡単に描画することができます。
丸に関しては Canvas
クラスの create_oval
メソッドを、三角に関しては Canvas
クラスの create_polygon
メソッドを利用することで描画できます。
具体的なサイズや配置の調整をどのようにしているかはサンプルスクリプトの draw_play_button
メソッドをご覧いただければと思います(かなりてきとうです…)。
再生ボタンの削除(delete_play_button
)
アプリ画面のキャンバス描画されている再生ボタンの削除を行います。
この再生ボタンの削除は Canvas
クラスの delete
メソッドにより、丸と三角を削除することで実現しています。
ファイル選択画面表示(select_open_file
)
またファイルを選択する際には、ファイル選択画面の表示も行います。
ファイル選択画面の表示については下記ページで解説していますので詳しく知りたい方はコチラを参照してください。
Python でファイル選択画面を表示するController
クラス
Controller
は下記を行うクラスです。
- イベントの受け付け(
set_events
) - 「動画選択」ボタンクリック時の処理(
push_load_button
) - 「モノクロON/OFF」ボタンクリック時の処理(
push_gray_button
) - 「フリップON/OFF」ボタンクリック時の処理(
push_flip_button
) - マウスクリック時の処理(
button_press
) - 定期的なフレーム進行処理(
frame_timer
) - 定期的な画像描画処理(
draw_timer
)
イベントの受け付け(set_events
)
Controller
クラスの主な責務の1つがユーザーからのイベントの受け付けになります。ですので、View
が設置したウィジェットに対してイベントの受け付け(bind
)を行います。
イベント受け付けを詳しく知りたい方は是非下のページも読んでみてください。
PythonでTkinterを使ってめちゃくちゃ簡単にGUIアプリを作る「動画選択」ボタンクリック時の処理(push_load_button
)
「動画選択」ボタンクリックイベントが発生した際には Contoller
クラスのオブジェクトは主に下記の処理を実行します。
View
にファイル選択画面の表示を依頼(select_open_file
)Model
に動画オブジェクトの生成を依頼(create_video
)- 先頭フレームの表示
(advance_frame
・create_image
・reverse_video
・draw_image
) Model
に再生ボタンの表示を依頼
(draw_play_button
・draw_play_button
)- フレーム進行用タイマー起動
- 描画用タイマー起動
フレーム進行用タイマーは「定期的なフレーム進行処理(frame_timer
)」を実行するためのもので、タイマーの時間は FPS(Model
クラスの提供する get_fps
メソッドで取得できた時間)から計算しています(単位は ms)。
# FPSに合わせてフレームを進める間隔を決定
fps = self.model.get_fps()
self.frame_timer = int(1 / fps * 1000 + 0.5)
# フレーム進行用のタイマースタート
self.master.after(self.frame_timer, self.frame)
描画用タイマーは「定期的な画像描画処理(draw_timer
)」を実行するためのもので 50 ms に設定しています。
「モノクロON/OFF」ボタンクリック時の処理(push_gray_button
)
「モノクロON/OFF」ボタンクリックイベントが発生した際には Contoller
クラスのオブジェクトは主に下記の処理を実行します。
Model
にモノクロ ON/OFF 設定切り替え依頼(set_gray
)
「フリップON/OFF」ボタンクリック時の処理(push_flip_button
)
「フリップON/OFF」ボタンクリックイベントが発生した際には Contoller
クラスのオブジェクトは主に下記の処理を実行します。
Model
にフリップ ON/OFF 設定切り替え依頼(set_flip
)
マウス操作時の処理(button_press
)
キャンバス上でマウスがクリックされた時には Contoller
クラスのオブジェクトは主に下記の処理を実行します。
- 動画再生中の場合
- 動画再生フラグを
False
にセット Model
に再生ボタンの表示を依頼(draw_play_button
)
- 動画再生フラグを
- 動画停止中の場合
- 動画再生フラグを
True
にセット Model
に再生ボタンの削除を依頼(delete_play_button
)
- 動画再生フラグを
定期的なフレーム進行処理(frame_timer
)
また Contoller
クラスのオブジェクトは一定間隔毎(「動画選択」ボタンクリック時の処理(push_load_button
)で決定した間隔)に下記の処理を実行します。
- タイマーの再起動
- 動画再生中の場合のみ下記を実行
Model
に動画のフレーム進行を依頼(advance_frame
)
動画が最後まで進行された場合は下記の処理も行います。
Model
に動画の巻き戻しを依頼(reverse_frame
)Model
に動画のフレーム進行を依頼(advance_frame
)
定期的な画像描画処理(draw_timer
)
また Contoller
クラスのオブジェクトは一定間隔毎(「動画選択」ボタンクリック時の処理(push_load_button
)で決定した間隔)に下記の処理を実行します。
- タイマーの再起動
- 動画再生中の場合のみ下記を実行
Model
に画像オブジェクト生成を依頼(create_image
)View
に画像描画を依頼(draw_image
)
ちなみに、定期処理を frame_timer
と draw_timer
の2つに分割しているのは、画像描画を行うための処理(create_image
、draw_image
)が FPS の速度に間に合わないことがあるためです。
この場合、動画のフレーム切り替えが遅い、フレーム飛びするなどの問題が発生してしまいますので、 draw_timer
は FPS からは切り離して時間設定(FPS よりも遅く画像描画を行うように設定)を行うようにしています。
create_image
の処理時間は print
で標準出力しています
draw_timer
の実行間隔はこの時間よりも余裕を持って設定してやればフレーム切り替えが遅い、フレーム飛びするなどの問題は防げるはずです
アプリのサンプルスクリプト
ここまで説明してきたアプリのサンプルスクリプトは下記になります。
import tkinter
import tkinter.filedialog
from PIL import Image, ImageTk
import cv2
import time
class Model():
def __init__(self):
# 動画オブジェクト参照用
self.video = None
# 画像処理の設定
self.gray = False
self.flip= False
# 読み込んだフレーム
self.frames = None
# PIL画像オブジェクト参照用
self.image = None
# Tkinter画像オブジェクト参照用
self.image_tk = None
def create_video(self, path):
'動画オブジェクトの生成を行う'
# pathの動画から動画オブジェクト生成
self.video = cv2.VideoCapture(path)
def advance_frame(self):
'フレームを読み込んで1フレーム進める'
if not self.video:
return
# フレームの読み込み
ret, self.frame = self.video.read()
return ret
def reverse_video(self):
'動画を先頭に戻す'
self.video.set(cv2.CAP_PROP_POS_FRAMES, 0)
def create_image(self, size):
'フレームの画像を作成'
t1 = time.time()
# フレームを読み込み
frame = self.frame
if frame is None:
print("None")
# 設定に応じて画像処理
if self.gray:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if self.flip:
frame = cv2.flip(frame, 1)
# PIL イメージに変換
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(rgb_frame)
# 指定サイズに合わせて画像をリサイズ
# 拡大率を計算
ratio_x = size[0] / pil_image.width
ratio_y = size[1] / pil_image.height
if ratio_x < ratio_y:
ratio = ratio_x
else:
ratio = ratio_y
# リサイズ
self.image = pil_image.resize(
(
int(ratio * pil_image.width),
int(ratio * pil_image.height)
)
)
t2 = time.time()
print(f"経過時間:{t2-t1}")
def get_image(self):
'Tkinter画像オブジェクトを取得する'
if self.image is not None:
# Tkinter画像オブジェクトに変換
self.image_tk = ImageTk.PhotoImage(self.image)
return self.image_tk
def get_fps(self):
'動画のFPSを取得する'
if self.video is None:
return None
return self.video.get(cv2.CAP_PROP_FPS)
def set_gray(self):
self.gray = not self.gray
def set_flip(self):
self.flip = not self.flip
class View():
def __init__(self, app, model):
self.master = app
self.model = model
# アプリ内のウィジェットを作成
self.create_widgets()
def create_widgets(self):
'アプリ内にウィジェットを作成・配置する'
# キャンバスのサイズ
canvas_width = 500
canvas_height = 300
# キャンバスとボタンを配置するフレームの作成と配置
self.main_frame = tkinter.Frame(
self.master
)
self.main_frame.pack()
# キャンバスを配置するフレームの作成と配置
self.canvas_frame = tkinter.Frame(
self.main_frame
)
self.canvas_frame.grid(column=1, row=1)
# ユーザ操作用フレームの作成と配置
self.operation_frame = tkinter.Frame(
self.main_frame
)
self.operation_frame.grid(column=2, row=1)
# キャンバスの作成と配置
self.canvas = tkinter.Canvas(
self.canvas_frame,
width=canvas_width,
height=canvas_height,
bg="#EEEEEE",
)
self.canvas.pack()
# ファイル読み込みボタンの作成と配置
self.load_button = tkinter.Button(
self.operation_frame,
text="動画選択"
)
self.load_button.pack()
# グレーON/OFFボタンの作成と配置
self.gray_button = tkinter.Button(
self.operation_frame,
text="モノクロON/OFF"
)
self.gray_button.pack()
# フリップ/OFFボタンの作成と配置
self.flip_button = tkinter.Button(
self.operation_frame,
text="フリップON/OFF"
)
self.flip_button.pack()
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"
)
def select_open_file(self, file_types):
'オープンするファイル選択画面を表示'
# ファイル選択ダイアログを表示
file_path = tkinter.filedialog.askopenfilename(
initialdir=".",
filetypes=file_types
)
return file_path
def draw_play_button(self):
'再生ボタンを描画'
# キャンバスのサイズ取得
width = self.canvas.winfo_width()
height = self.canvas.winfo_height()
# 円の直径を決定
if width > height:
diameter = height
else:
diameter = width
# 端からの距離を計算
distance = diameter / 10
# 円の線の太さを計算
thickness = distance
# 円の描画位置を決定
sx = (width - diameter) // 2 + distance
sy = (height - diameter) // 2 + distance
ex = width - (width - diameter) // 2 - distance
ey = height - (height - diameter) // 2 - distance
# 丸を描画
self.canvas.create_oval(
sx, sy,
ex, ey,
outline="white",
width=thickness,
tag="oval"
)
# 頂点座標を計算
x1 = sx + distance * 3
y1 = sy + distance * 2
x2 = sx + distance * 3
y2 = ey - distance * 2
x3 = ex - distance * 2
y3 = height // 2
# 三角を描画
self.canvas.create_polygon(
x1, y1,
x2, y2,
x3, y3,
fill="white",
tag="triangle"
)
def delete_play_button(self):
self.canvas.delete("oval")
self.canvas.delete("triangle")
class Controller():
def __init__(self, app, model, view):
self.master = app
self.model = model
self.view = view
# 動画再生中かどうかの管理
self.playing = False
# フレーム進行する間隔
self.frame_timer = 0
# 描画する間隔
self.draw_timer = 50
self.set_events()
def set_events(self):
'受け付けるイベントを設定する'
# キャンバス上のマウス押し下げ開始イベント受付
self.view.canvas.bind(
"<ButtonPress-1>",
self.button_press
)
# 動画選択ボタン押し下げイベント受付
self.view.load_button['command'] = self.push_load_button
# モノクロON/OFFボタン押し下げイベント受付
self.view.gray_button['command'] = self.push_gray_button
# フリップON/OFFボタン押し下げイベント受付
self.view.flip_button['command'] = self.push_flip_button
def draw(self):
'一定間隔で画像等を描画'
# 再度タイマー設定
self.master.after(self.draw_timer, self.draw)
# 動画再生中の場合
if self.playing:
# フレームの画像を作成
self.model.create_image(
(
self.view.canvas.winfo_width(),
self.view.canvas.winfo_height()
)
)
# 動画1フレーム分をキャンバスに描画
self.view.draw_image()
def frame(self):
'一定間隔でフレームを進める'
# 再度タイマー設定
self.master.after(self.frame_timer, self.frame)
# 動画再生中の場合
if self.playing:
# 動画を1フレーム進める
ret = self.model.advance_frame()
# フレームが進められない場合
if not ret:
# フレームを最初に戻す
self.model.reverse_video()
self.model.advance_frame()
def push_load_button(self):
'動画選択ボタンが押された時の処理'
file_types = [
("MOVファイル", "*.mov"),
("MP4ファイル", "*.mp4"),
]
# ファイル選択画面表示
file_path = self.view.select_open_file(file_types)
if len(file_path) != 0:
# 動画オブジェクト生成
self.model.create_video(file_path)
# 最初のフレームを表示
self.model.advance_frame()
self.model.create_image(
(
self.view.canvas.winfo_width(),
self.view.canvas.winfo_height()
)
)
self.model.reverse_video()
self.view.draw_image()
# 再生ボタンの表示
self.view.delete_play_button()
self.view.draw_play_button()
# FPSに合わせてフレームを進める間隔を決定
fps = self.model.get_fps()
self.frame_timer = int(1 / fps * 1000 + 0.5)
# フレーム進行用のタイマースタート
self.master.after(self.frame_timer, self.frame)
# 画像の描画用のタイマーセット
self.master.after(self.draw_timer, self.draw)
def button_press(self, event):
'マウスボタン押された時の処理'
# 動画の再生/停止を切り替える
if not self.playing:
self.playing = True
# 再生ボタンの削除
self.view.delete_play_button()
else:
self.playing = False
# 再生ボタンの描画
self.view.draw_play_button()
def push_gray_button(self):
self.model.set_gray()
def push_flip_button(self):
self.model.set_flip()
app = tkinter.Tk()
app.title("動画再生アプリ")
model = Model()
view = View(app, model)
controller = Controller(app, model, view)
app.mainloop()
実行すると下記のような画面が表示されます。
「動画選択」ボタンを押せばファイル選択画面が表示されるので、動画ファイル(.MOV or .MP4)を選んでください。選んだ動画の先頭フレームと動画再生ボタンがキャンバスに描画されます。
キャンバス(動画再生ボタン)をクリックすると、動画の再生が始まります。
「モノクロON/OFF」ボタンをクリックすると画像のカラー・モノクロが切り替わります。
また「フリップON/OFF」ボタンをクリックすると画像の左右が反転します。
またキャンバスをクリックすると動画の再生が停止し、動画再生ボタンが表示されます。
スポンサーリンク
まとめ
このページでは Python で動画を再生するアプリ開発の説明を行いました。
Python での動画再生だけでなく、GUI アプリの作り方やタイマーを利用した画像のリアルタイム描画処理についても学べる良いテーマだと思います!
今回は動画に対してモノクロ処理・フリップ処理を行いましたが、OpenCV2 を利用して顔認識などを行いながら動画再生を行うようなことも可能です(動画の顔部分を色をつけて表示するなど)。
様々な発展のさせ方があると思いますので、是非色々カスタマイズを行い、Python や動画・画像・認識処理についての学習に役立ててください!