Python, OpenCVで動画再生時のFPS(フレームレート)を測定・表示
OpenCVで動画(ファイルの映像やカメラのリアルタイム映像)を再生する際のFPS(Frames Per Second: 1秒あたりのフレーム数=フレームレート)は様々な要因で変動するため、実効的なFPSを知るには測定・表示する必要がある。
ここでは以下の内容について説明する。
- 動画ファイルやカメラのFPS設定を取得・変更
- FPSの取得
- FPSの変更
- OpenCVにおける動画再生(表示)
- 動画ファイルの場合
- カメラの場合
- 入力フレームと出力フレームの対応関係
- 動画ファイルの場合
- カメラの場合
- 動画再生時のFPSを測定、描画して表示
- 動画再生時のFPSに影響する要素
waitKey()
- リアルタイム画像処理
OpenCVにおける動画の読み込みや基本的な扱いについて以下の記事を参照。
動画ファイルやカメラのFPS設定を取得・変更
VideoCapture
オブジェクトとして開いた動画ファイルやカメラデバイスのFPS設定はget()
, set()
で取得・変更することができる。後述のように、OpenCVで動画を再生(表示)する場合、VideoCapture
オブジェクトに設定されているFPSの値の通りに再生されるわけではないので注意。
FPSの取得
動画ファイルやカメラに設定されたFPSを取得するにはVideoCapture
オブジェクトのget()
メソッドを使う。
指定するプロパティはCAP_PROP_FPS
(OpenCV2の場合はCV_CAP_PROP_FPS
)。
import cv2
cap = cv2.VideoCapture(0)
print(cap.get(cv2.CAP_PROP_FPS))
# 29.000049
例はカメラの場合だが、動画ファイルでも同じようにFPSを取得して確認できる。
FPSの変更
VideoCapture
オブジェクトのset()
メソッドを使うとプロパティを指定して値を変更できる。
すべてのプロパティが変更できるわけではなく、動画ファイルの場合、CAP_PROP_FPS
は変更できない。set()
メソッドで指定してもFalse
が返される。
カメラの場合はCAP_PROP_FPS
を変更可能。set()
メソッドがTrue
を返し、新たな値が設定されていることが確認できる。
print(cap.set(cv2.CAP_PROP_FPS, 10))
# True
print(cap.get(cv2.CAP_PROP_FPS))
# 10.0
set()
メソッドがTrue
を返しても指定した値に変更されるとは限らないので注意。カメラが対応していないFPSを指定すると、set()
メソッドはTrue
を返すが、値はそのとおりに変更されない。
print(cap.set(cv2.CAP_PROP_FPS, 120))
# True
print(cap.get(cv2.CAP_PROP_FPS))
# 30.00003
OpenCVにおける動画再生(表示)
OpenCVには動画を再生(表示)する特別なメソッドはない。VideoCapture
オブジェクトのread()
メソッドで読み込んだ画像の配列を順次imshow()
で表示していくだけ。
例えば以下のコードで動画を再生できる。どちらもキーボードのq
を押すと終了する。コードの詳細は以下の記事を参照。
動画ファイルの場合
import cv2
import sys
file_path = 'data/temp/sample_video.mp4'
delay = 1
window_name = 'frame'
cap = cv2.VideoCapture(file_path)
if not cap.isOpened():
sys.exit()
while True:
ret, frame = cap.read()
if ret:
cv2.imshow(window_name, frame)
if cv2.waitKey(delay) & 0xFF == ord('q'):
break
else:
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
cv2.destroyWindow(window_name)
カメラの場合
import cv2
import sys
camera_id = 0
delay = 1
window_name = 'frame'
cap = cv2.VideoCapture(camera_id)
if not cap.isOpened():
sys.exit()
while True:
ret, frame = cap.read()
cv2.imshow(window_name, frame)
if cv2.waitKey(delay) & 0xFF == ord('q'):
break
cv2.destroyWindow(window_name)
CAP_PROP_FPS
でFPSをデフォルトの値より小さくすると、画像を取得できずread()
が(False, None)
を返すことがある(read()
できたりできなかったりする)。
現実的にはカメラのFPSをあえて下げることはあまりないが、そのような場合は以下のように画像が取得できたときのみ表示するようにする。画像が取得できないときは無視するため画像が全く取得できない場合は強制終了するしかなくなるので注意。
if ret:
cv2.imshow(window_name, frame)
入力フレームと出力フレームの対応関係
動画を再生する際、あるフレームの画像をimshow()
で表示してから次のフレームの画像をimshow()
で表示するまでの間にwaitKey()
やその他の処理によって待ち時間が発生する。
元の動画のフレーム(入力フレーム)と再生時に表示されるフレーム(出力フレーム)の対応関係は、動画ファイルを再生するかカメラのリアルタイム映像を再生するかによって異なる。
動画ファイルの場合
動画ファイルの場合はread()
することで次のフレームに進む。
例えば0
から9
までの10フレームからなる動画ファイルを例とする。待ち時間を-
で示すと、元の動画のフレームと表示されるフレームの対応関係は以下のようになる。
in: 0123456789
out: 0--1--2--3--4--5--6--7--8--9
動画ファイルを再生する場合は必ずすべてのフレームが表示される。待ち時間が長くなるとスロー再生やコマ送りのようになり、待ち時間が短いと高速に再生される。
元動画よりも高速で再生されるのは以下のようなイメージ。例えば元動画が30FPSだと1フレームの表示時間は33.3ミリ秒(1 / 30秒)だが、表示時の待ち時間が16.7ミリ秒程度(60FPS相当)だと倍の速度で再生される。
in: 0---1---2---3---4---5---6---7---8---9
out: 0-1-2-3-4-5-6-7-8-9
このように、動画ファイルをOpenCVで再生する際のFPSは元の動画に設定されたFPSとは関係なく、各フレームを表示するのにかかる経過時間(待ち時間)のみに依存する。
カメラの場合
カメラからリアルタイム映像が入力される場合はread()
するしないに関わらず映像は常に更新されている。read()
して読み込まれるのはそのタイミング以降に入力される最新のフレームとなる。
便宜上、カメラから入力されるフレームに0
から9
までの番号をつける。待ち時間を-
で示すと、カメラのフレームと表示されるフレームの対応関係のイメージは以下のようになる。
カメラのFPSが高くても再生時の待ち時間が長いと飛び飛びのフレームしか表示できない。表示されるFPSはカメラのFPSよりも低くなる。
in: 0123456789...
out: 0--3--6--9...
read()
を実行するとそこから新しいフレームが入力されるまで自動的に待つことになるため、カメラのFPSが低い場合でも同じフレームが2回表示されることはなく、カメラのFPSよりも高いFPSで表示されることはない。以下のようなイメージ。
in: 0---1---2---3---4---5---6---7---8---9...
out: 0---1---2---3---4---5---6---7---8---9...
ただし、カメラのFPS設定をあまりに低くすると画像が取得できないタイミングが発生する場合もある模様。
動画再生時のFPSを測定、描画して表示
実際にOpenCVで再生(表示)されている動画のFPSを測定し、画像に描画して重畳表示する方法を説明する。
経過時間の測定にはcv2.TickMeter
クラス、文字の描画にはcv2.putText()
関数を使う。
以下のようなコードとなる。例はカメラの場合だが、動画ファイルでも考え方は同じ。
import cv2
import sys
camera_id = 0
delay = 1
window_name = 'frame'
cap = cv2.VideoCapture(camera_id)
if not cap.isOpened():
sys.exit()
tm = cv2.TickMeter()
tm.start()
count = 0
max_count = 10
fps = 0
while cap.isOpened():
ret, frame = cap.read()
if count == max_count:
tm.stop()
fps = max_count / tm.getTimeSec()
tm.reset()
tm.start()
count = 0
cv2.putText(frame, 'FPS: {:.2f}'.format(fps),
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), thickness=2)
cv2.imshow(window_name, frame)
count += 1
if cv2.waitKey(delay) & 0xFF == ord('q'):
break
cv2.destroyWindow(window_name)
FPSはFrames Per Secondなので、「フレーム数」を「そのフレームを表示するのに掛かった時間(秒)」で割れば求められる。
毎フレームFPSを算出して表示すると値が目まぐるしく変わるため、サンプルコードでは数フレーム分(変数max_count
で指定)で算出している。
経過時間の測定にはcv2.TickMeter
クラスを使用。
コンストラクタcv2.TickMeter()
でオブジェクトを生成し、start()
で開始、stop()
で終了、getTimeSec()
でその経過時間(秒)を取得できる。再度測定を開始する場合はstart()
の前にreset()
が必要。ミリ秒やマイクロ秒で経過時間を返すメソッドgetTimeMilli()
, getTimeMicro()
もある。
文字の描画はcv2.putText()
関数。文字色や位置を変えたい場合は以下の記事を参照。
動画再生時のFPSに影響する要素
OpenCVにおける動画再生はあくまでもread()
で読み込んだ画像を順次imshow()
で表示するだけなので、動画再生時のFPSは、
- 環境(使用しているマシンの性能など)
waitKey()
の引数に指定する待ち時間- その他の処理
などの影響によって変わる。
waitKey()
上のサンプルコードでは変数delay
でwaitKey()
の引数を指定するようになっている。
例えばdelay = 1000
でwaitKey(1000)
とすると1000ミリ秒つまり1秒おきに1フレーム分の画像が表示されコマ送りのようになる(概ね1FPS)。動画ファイルの再生ではすべてのフレームが1秒おきに表示され、カメラ映像の再生では1秒おきにそのタイミングで入力されるフレームの画像が表示されることになる。
ただし、waitKey()
の引数を小さくすればするほど再生時のFPSが大きくなるわけではない。
例えば上の例でdelay = 1
(waitKey(1)
=待ち時間が1ミリ秒)としても実際には1秒に1000フレーム表示されることはない(1000FPSにはならない)。
これはwaitKey()
の処理自体に時間がかかるため。
公式のフォーラムでもwaitKey(1)
としても十数ミリ秒の待ち時間が発生することが報告されている(環境によって異なる)。
waitKey()
が待ち時間を正確に制御できないことは公式ドキュメントにも明記されている。
the function will not wait exactly delay ms
OpenCV: High-level GUI - waitKey()
60FPSが1フレームあたり16.7ミリ秒(1/60秒)なので、imshow()
とwaitKey()
を使うとだいたいそれくらいが限界になる(
imshow()
で画像を表示する場合waitKey()
は必須)。高FPSを実現したい場合はOpenCV以外の手段で実装する必要がある。
waitKey(1)
としておけば可能な限り最小の待ち時間(最大のFPS)で表示されるため、カメラの映像をリアルタイムで表示する場合は基本的にはwaitKey(1)
としておけば問題ないはず。
動画ファイルを再生する場合はファイルの1フレームあたりの時間(FPSの逆数)と同じかそれより少し小さい値をwaitKey()
設定すると概ね正しいFPSで再生される。例えば30FPSのファイルをwaitKey(1)
とすると、環境によって異なるが、上述のように60FPS程度(2倍速)で再生される場合があるので注意。
リアルタイム画像処理
read()
で画像を読み込んでからimshow()
で表示するまでの間に様々な処理を行うことで、リアルタイムに画像を処理できる。
リアルタイムで画像処理を行うとその処理による待ち時間が発生するため1フレーム表示するのにかかる時間が長くなりFPSは低下する。当然ながらRaspberry Piのような非力なマシンであれば処理時間が長くなりFPSはより低くなる。