Python, OpenCVで動画ファイルからフレームを切り出して保存
Python, OpenCVでmp4やaviなどの動画ファイルからフレームを切り出して静止画の画像ファイルとして保存する方法について説明する。
単純に動画をフレームごとの静止画として切り出すだけならffmpegのコマンドで可能だが、何らかの画像処理を行ってから保存したい場合はPython + OpenCVを使うと便利。
ここでは以下のサンプルコードを示す。
- すべてのフレームを画像ファイルとして保存
- 任意のフレームを指定して画像ファイルとして保存
- 単独のフレームを指定
- 任意の範囲のフレームを指定
- 時間(秒数)で指定
- 動画を再生しながらキーボード押下で保存
動画ファイルではなく、カメラ(内蔵カメラ・USBカメラ・Webカメラ)のリアルタイム映像から静止画を切り出して保存したい場合は以下の記事を参照。
OpenCVにおける動画の読み込みや基本的な扱いについて以下の記事を参照。
以下のサンプル動画を例として使っている。
すべてのフレームを画像ファイルとして保存
動画ファイルのすべてのフレームを静止画の画像ファイルとして切り出して保存するサンプルコード。指定したディレクトリに<basename>_<連番>.<拡張子>
というファイル名で保存する。
import cv2
import os
def save_all_frames(video_path, dir_path, basename, ext='jpg'):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return
os.makedirs(dir_path, exist_ok=True)
base_path = os.path.join(dir_path, basename)
digit = len(str(int(cap.get(cv2.CAP_PROP_FRAME_COUNT))))
n = 0
while True:
ret, frame = cap.read()
if ret:
cv2.imwrite('{}_{}.{}'.format(base_path, str(n).zfill(digit), ext), frame)
n += 1
else:
return
save_all_frames('data/temp/sample_video.mp4', 'data/temp/result', 'sample_video_img')
save_all_frames('data/temp/sample_video.mp4', 'data/temp/result_png', 'sample_video_img', 'png')
os.makedirs()
でディレクトリを作成する。引数exist_ok
はPython 3.2より前のバージョンでは使用できないので注意。
パス文字列の結合はos.path.join()
。
総フレーム数をCAP_PROP_FRAME_COUNT
(OpenCV2ではCV_CAP_PROP_FRAME_COUNT
)で取得し、その桁数を算出。その桁数に応じてzfill()
でゼロ埋めする。
- 関連記事: Pythonで数値の桁数、任意の桁(位)の値を取得
- 関連記事: Pythonで文字列・数値をゼロ埋め(ゼロパディング)
任意のフレームを指定して画像ファイルとして保存
単独のフレームを指定
フレーム番号を指定して静止画の画像ファイルとして切り出して保存するサンプルコード。この例では保存先ディレクトリやファイル名、拡張子をまとめて保存先のパスとして指定するようにしている。
def save_frame(video_path, frame_num, result_path):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return
os.makedirs(os.path.dirname(result_path), exist_ok=True)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num)
ret, frame = cap.read()
if ret:
cv2.imwrite(result_path, frame)
save_frame('data/temp/sample_video.mp4', 100, 'data/temp/result_single/sample_100.jpg')
os.path.dirname()
で保存先のパスのディレクトリを取得してos.makedirs()
で作成。
任意の範囲のフレームを指定
任意の範囲のフレームを静止画の画像ファイルとして切り出して保存するサンプルコード。
def save_frame_range(video_path, start_frame, stop_frame, step_frame,
dir_path, basename, ext='jpg'):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return
os.makedirs(dir_path, exist_ok=True)
base_path = os.path.join(dir_path, basename)
digit = len(str(int(cap.get(cv2.CAP_PROP_FRAME_COUNT))))
for n in range(start_frame, stop_frame, step_frame):
cap.set(cv2.CAP_PROP_POS_FRAMES, n)
ret, frame = cap.read()
if ret:
cv2.imwrite('{}_{}.{}'.format(base_path, str(n).zfill(digit), ext), frame)
else:
return
save_frame_range('data/temp/sample_video.mp4',
200, 300, 10,
'data/temp/result_range', 'sample_video_img')
引数step_frame
で100フレーム毎などの設定も可能。開始フレームstart_frame
、終了フレームstop_frame
およびstep_frame
はそのままrange()
に渡している。
- 関連記事: Pythonのrange関数の使い方
時間(秒数)で指定
これまでの例はフレーム番号で指定しているが、秒数で指定したい場合はFPSをもとに計算すればよい。
FPS = frame / time[sec]
なので、time[sec] = frame / FPS
となる。フレーム番号は整数である必要があるためround()
で丸める。指定した秒数との間には誤差が発生するので注意。
あとはこれまでの例とほぼ同じ。
単独のフレームを指定。ここでは出力先のパスを引数で指定するようにしている。フレーム番号や実際の秒数を加えたい場合は次の例を参照。
def save_frame_sec(video_path, sec, result_path):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return
os.makedirs(os.path.dirname(result_path), exist_ok=True)
fps = cap.get(cv2.CAP_PROP_FPS)
cap.set(cv2.CAP_PROP_POS_FRAMES, round(fps * sec))
ret, frame = cap.read()
if ret:
cv2.imwrite(result_path, frame)
save_frame_sec('data/temp/sample_video.mp4', 10, 'data/temp/result_10sec.jpg')
範囲を指定。range()
には整数int
しか使えないので、秒数を浮動小数点数float
で指定できるようにwhile
文を使用する。
def save_frame_range_sec(video_path, start_sec, stop_sec, step_sec,
dir_path, basename, ext='jpg'):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return
os.makedirs(dir_path, exist_ok=True)
base_path = os.path.join(dir_path, basename)
digit = len(str(int(cap.get(cv2.CAP_PROP_FRAME_COUNT))))
fps = cap.get(cv2.CAP_PROP_FPS)
fps_inv = 1 / fps
sec = start_sec
while sec < stop_sec:
n = round(fps * sec)
cap.set(cv2.CAP_PROP_POS_FRAMES, n)
ret, frame = cap.read()
if ret:
cv2.imwrite(
'{}_{}_{:.2f}.{}'.format(
base_path, str(n).zfill(digit), n * fps_inv, ext
),
frame
)
else:
return
sec += step_sec
save_frame_range_sec('data/temp/sample_video.mp4',
2, 10, 2,
'data/temp/result_range_sec', 'sample_video_img')
出力されるパス(ファイル名)にはフレーム番号と対応する秒数を含めている。秒数はあくまでも近似値なので注意。この例では{:.2f}
で小数点以下2桁までを出力。
動画を再生しながらキーボード押下で保存
動画を再生して確認しながらキーボードを押下したタイミングのフレームを静止画の画像ファイルとして切り出して保存するサンプルコード。
エンドレスリピートで動画が再生され、キーボードのq
を押すと終了、c
を押すと静止画が保存される。waitKey()
に渡すdelay
を0にすると、q
, c
以外のキーを押すたびにフレームが進むようになる。
import cv2
import os
def save_frame_play(video_path, dir_path, basename, ext='jpg', delay=1, window_name='frame'):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return
os.makedirs(dir_path, exist_ok=True)
base_path = os.path.join(dir_path, basename)
digit = len(str(int(cap.get(cv2.CAP_PROP_FRAME_COUNT))))
n = 0
while True:
ret, frame = cap.read()
if ret:
cv2.imshow(window_name, frame)
key = cv2.waitKey(delay) & 0xFF
if key == ord('c'):
cv2.imwrite('{}_{}.{}'.format(base_path, str(n).zfill(digit), ext), frame)
elif key == ord('q'):
break
n += 1
else:
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
n = 0
cv2.destroyWindow(window_name)
save_frame_play('data/temp/sample_video.mp4', 'data/temp', 'sample_video_cap', delay=0)
動画の再生についての詳細は以下の記事を参照。read()
で読み出した画像をimshow()
で表示しているだけ。