PySimpleGUIのウィンドウ上で動画を再生する方法と、ついでにultralyticsのYOLOv8を使った簡単なデモを紹介します。この記事はPySimpleGUIのScreens2で紹介されているものを参考にしています。GitHubへのリンクはこちらです。
環境
- Windows11
- Python 3.11.0
- PySimpleGUI 4.60.5
- ultralytics 8.0.124
ロードマップ
だいたい以下の流れで話を進めます。
- OpenCVの
VideoCapture
クラスの簡単な使い方について - PySimpleGUIで動画を表示する方法について
- YOLOv8のデモ
opencv-python:VideoCaptureクラス
opencv-python
はコンピュータビジョンの分野でよく知られた、OpenCVのPythonパッケージです。詳しくはこちらをどうぞ。公式のチュートリアルも充実しているので、そっちを見たほうが早いかもしれません。
opencv-python
で動画を扱うにはVideoCapture
クラスを利用します。以下は動画ファイルを再生するスクリプトです。
import cv2 as cv
path = '動画ファイルへのパス'
vc = cv.VideoCapture(path)
while vc.isOpened():
ret, flame = vc.read()
if ret:
cv.imshow('test', flame)
cv.waitKey(25)
else:
print('フレームが読み込めないか終端に達したかしました')
break
vc.release()
cv.destroyAllWindows ()
VideoCapture
クラスのコンストラクタには、「動画ファイルのパス」かwebカメラなどのデバイスを示す「整数値」かのどちらかを渡します。単に動画を表示したいというだけなら、VideoCapture
クラスについては次の三つの関数を押さえておけば十分です。
-
VideoCapture.isOpened()
動画ファイルが正しく開けているかどうかをBoolで返す -
VideoCapture.read()
動画を構成するフレーム画像を「一枚だけ」numpy
配列として、さらに画像を正しく取得できたかどうかを示すBoolを返す -
VideoCapture.release()
動画ファイルを閉じる
read()
は呼び出されるたびに動画の並びに沿って順番にフレーム画像を返します。最初の呼び出しでは先頭のフレームを、二度目の呼び出しでは二番目のフレームを、三度目には…といった格好です。動画の再生は、このフレーム画像をループを使って順次表示してやれば実現できます。
フレーム画像の表示には次の二つの関数を使います。
-
cv.imshow()
画像を表示するウィンドウを生成し、引数で指定したnumpy
配列による画像を表示する。最初の引数はウィンドウタイトルです -
cv.waitKey()
引数で指定した時間(ミリ秒)だけ、キーボードの入力を待機する
cv.waitKey()
の待機時間は動画のフレームレートに応じて選ぶ必要があります。チュートリアルでは、一般に25ミリ秒程度でよいとあります。フレームレート30(一秒間の表現に30枚の画像)の動画であれば、一枚の画像が1/30秒(≒33ミリ秒)で切り替わるので、read()
やimshow()
の処理を加味すればだいたい適当な間隔で表示できるだろう、ということだと思います。
PySimpleGUIでの動画の表示
画像の表示とその切り替えさえできれば、動画の再生が可能になることがわかりました。PySimpleGUIには画像を表示するためのImage
エレメントクラスがあるので、動画の表示もまた可能になります(PySimpleGUIの基本的な扱い方についてはほかの記事を参照してください)。
Imageエレメントの要点は大体次のようなものです。
- 表示したい画像をパスかバイト列で指定する
- エレメントの内容を変更する
update()
関数にイメージデータを直接渡すことができる
ここで問題になるのが、opencv-python
とPySimpleGUIでの画像の形式の違いです。numpy
配列の画像をバイト列に変換する必要があります。
-
cv.imencode()
numpy
配列による画像表現を、指定した画像ファイルの形式に変換する
あとは取得したフレーム画像をエンコードして、イメージエレメントに渡してやるだけです。
image_element = PySimpleGUI.Image(key='IMAGE')
このようなイメージエレメントを用意して、メッセージループの中で次のようにアップデートを繰り返えせば動画が表示されます。
while True:
ret, flame = vc.read()
if ret:
img = cv.imencode('.png', flame)[1].tobytes()
window['IMAGE'].update(img)
cv.waitKey(25)
else:
print('フレームが読み込めないか終端に達したかしました')
break
ただし、このループをボタンエレメントなどのイベントとしてメッセージループに組み込むと、動画の再生中は何の操作も受け付けないものになってしまいます。
そこで、メッセージループにタイムアウト設定を追加して、read()
の呼び出しをメッセージループの一連の処理に組み込んでしまいます。
import cv2 as cv
import PySimpleGUI as sg
path = '動画へのパス'
switch = False
layout = [[sg.Text('', key='TEXT')],
[sg.Button('読み込み', key='BUTTON1')],
[sg.Button('停止', key='BUTTON2', disabled=True)],
[sg.Image(key='IMAGE', size=(400, 400))]]
window = sg.Window('v_cap_test', layout)
while True:
event, values = window.read(timeout=0)
if event in (sg.WIN_CLOSED, 'Exit'):
if switch:
vc.release()
break
if event == 'BUTTON1':
vc = cv.VideoCapture(path)
switch = vc.isOpened()
window['BUTTON2'].update(disabled=False)
window['BUTTON1'].update(disabled=True)
if event == 'BUTTON2':
vc.release()
switch = False
window['BUTTON2'].update(disabled=True)
window['BUTTON1'].update(disabled=False)
if switch:
ret, flame = vc.read()
if ret:
img = cv.imencode('.png', flame)[1].tobytes()
window['IMAGE'].update(img)
else:
print('動画が読み込めないか終端に達したかしました')
window.close()
window.read()
関数のtimeout=
に値をセットすると、メッセージの待機を指定した時間(ミリ秒)で切り上げるようになります(この時__TIMEOUT__
というイベントが発生します)。
このメッセージループの処理の一つに、イベントとは関係なくVideoCapture.read()
を呼び出す処理を加えれば、cv.waitKey()
で時間調整をしなくても、適切な速度で画像の切り替えが実行できます。
なお、上の例ではtimeout=0
になっていますが、このままスクリプトを実行してもやや早いかな、くらいの速度で動画が再生できます。これは以下のようなことが理由だと思われます。
- 都度行われる
if
の評価に一定の時間がいる -
imencode()
とupdate()
に時間がかかる
簡単な計測を行ったところ、imencode()
とupdate()
の実行だけで、それぞれに10ミリ秒弱かかっていました。正直、動画を表示するだけにしては時間がかかりすぎてるように思います。画像の変換処理などを加えた場合さらに時間がかかるわけですから、PySimpleGUI向きではないのかもしれません。
YOLOv8への応用
最後にultralytics
のYOLOv8で遊んでみようと思います。ultralytics
ではシンプルなインターフェイスから訓練済みのYOLOv8モデルを利用できます。詳しくはこちらから。
import cv2 as cv
import PySimpleGUI as sg
from ultralytics import YOLO
path = '動画へのパス'
switch = False
font = cv.FONT_HERSHEY_SIMPLEX
model = YOLO('yolov8n.pt')
layout = [[sg.Text('', key='TEXT')],
[sg.Button('読み込み', key='BUTTON1')],
[sg.Button('停止', key='BUTTON2', disabled=True)],
[sg.Image(key='IMAGE', size=(400, 400))]]
window = sg.Window('v_cap_test', layout)
def draw_res(img, results):
for i in results:
for j in i.boxes:
xy = j.xyxy.numpy().astype(int)
cv.rectangle(img, (xy[0][0], xy[0][3]), (xy[0][2], xy[0][1]), (0,0,255), 4)
cls = int(j.cls)
cv.putText(img, model.model.names[cls], (xy[0][0], xy[0][3]), font, 1,(0, 0, 0),2,cv.LINE_AA)
return 0
while True:
event, values = window.read(timeout=0)
if event in (sg.WIN_CLOSED, 'Exit'):
if switch:
vc.release()
break
if event == 'BUTTON1':
vc = cv.VideoCapture(path)
switch = vc.isOpened()
window['BUTTON2'].update(disabled=False)
window['BUTTON1'].update(disabled=True)
if event == 'BUTTON2':
vc.release()
switch = False
window['BUTTON2'].update(disabled=True)
window['BUTTON1'].update(disabled=False)
if switch:
ret, flame = vc.read()
if ret:
results = model(flame, device='cpu')
draw_res(flame, results)
img = cv.imencode('.png', flame)[1].tobytes()
window['IMAGE'].update(img)
else:
print('フレームが読み込めないか終端に達したかしました')
window.close()
最初に実行するときにyolov8n
モデルのダウンロードが自動的に行われます。
公式のデータでは、速度重視のモデルYOLOv8n
であっても、GPUがないと一枚の画像の処理(この場合物体検出)に80ミリ秒かかるとあります。私の環境では100ミリ秒ちょっとと、さらに動画の表示にかかる時間も含めて、1フレームあたり150ミリ秒ほど必要でした。
参考
- https://github.com/chanon-kr/simple_imagui_app
- https://docs.opencv.org/4.6.0/d6/d00/tutorial_py_root.html
- https://docs.ultralytics.com/
-
https://github.com/opencv/opencv/tree/master/samples/data
動画のサンプルにOpenCVのものを使いました(最後のスクリーンショットにしか登場しませんが)。