こんにちは。技術研究所の910です。
先日弊社にて開催いたしましたオープンハウスでは、動画を活用した研究に関する発表をさせていただきました。
なので今回は動画にフォーカスを当てて、動画に含まれるkeyframeのindexの取得を試してみました。
keyframeについてはこちらの記事に分かり易くまとめてくださっています。
- 環境
(cv2) ysk@LAKuEN$ sw_vers && python -V |
ProductName: Mac OS X |
ProductVersion: 10.13.3 |
BuildVersion: 17D102 |
Python 3.6.5 |
- データ
検証に使わせていただいたデータは、Pixabayにて公開されているこちらの動画になります。
こちらのファイルをFireworks – 348.mp4として使いました。
まずはffprobe
コマンドで、動画内の全フレームの情報を取得してみます。
# FFmpegの導入: FFmpegを導入すると一緒にffprobeが導入される |
(cv2) ysk@LAKuEN$ brew install ffmpeg</code> <code># 動画内の全フレームの情報をjson形式で取得 |
(cv2) ysk@LAKuEN$ ffprobe -show_frames -select_streams v -print_format json "./Fireworks - 348.mp4" > output_with_cli.json |
これで、output_with_cli.jsonに動画内の全フレームの情報が書き出されます。
以下は出力されたjsonの一部となります。
json内のkey_frameキーを見ると、そのフレームがkeyframeか否かが判別できます。
また、keyframeか否かだけではなく、フレームの種別迄確認する際にはpict_typeキーで確認できます。
{ |
"frames": [ |
{ |
"media_type": "video", |
"stream_index": 0, |
"key_frame": 1, # 1: keyframeである, 0: keyframeでない |
"pkt_pts": 0, |
"pkt_pts_time": "0.000000", |
"pkt_dts": 0, |
"pkt_dts_time": "0.000000", |
"best_effort_timestamp": 0, |
"best_effort_timestamp_time": "0.000000", |
"pkt_duration": 1, |
"pkt_duration_time": "0.040000", |
"pkt_pos": "6781", |
"pkt_size": "23688", |
"width": 1280, |
"height": 720, |
"pix_fmt": "yuv420p", |
"sample_aspect_ratio": "1:1", |
"pict_type": "I", # フレームの種別 |
"coded_picture_number": 0, |
"display_picture_number": 0, |
"interlaced_frame": 0, |
"top_field_first": 0, |
"repeat_pict": 0, |
"color_range": "tv", |
"color_space": "bt709", |
"color_primaries": "bt709", |
"color_transfer": "bt709", |
"chroma_location": "left" |
}, |
... |
] |
} |
ffprobeで取得できる情報については、こちらに詳細が記載されています。
これをPythonで読み込む為にはjson.loads()を使えばdictとして簡単に読み込めますが、Pythonのdictには順序が保たれないという特徴があります。
実際にjson.loads()
を使ってdictにすると、jsonファイルの順序と異なる順序になり得るようで、collections.OrderedDictに直接変換するようにすることで対処できるようです。
【参考】jsonの順序を保ったままOrderedDictを作る
また、別の手段としては一旦dictとして読み込み、その後pkt_ptsキーで昇順にソートしてOrderedDictにすることでも順序を直せます。
※ DTS(decoding time stamp)、PTS(presentation time stamp) についてはこちらを参照しました。
【参考】Qiita: ffmpeg を使うなら知っておきたい話 PTSとかDTSの話:音ずれ問題や時間が変になるときのために ヽ(゚ー゚ヽ)(ノ゚ー゚)ノわぁい
全フレームの情報を取得する為のコマンドは分かったので、この中から必要なkey_frameキーのみを取り出して、どのフレームがkeyframeなのかを調べてみます。
ようやくここからPythonを使います。
import codecs |
import json |
import subprocess |
def detect_keyframe_with_ffprobe(filepath): |
command = 'ffprobe -show_frames -select_streams v -print_format json "{}"'.format(filepath) |
try: |
# コマンドを実行して結果をCompletedProcessオブジェクトとして取得 |
response = subprocess.run( |
command, # 実行するコマンド |
shell=True, # shell上で実行 |
check=True, # 終了コードが非ゼロの場合CalledProcessErrorをraise |
stdout=subprocess.PIPE, # 処理結果をPIPEに渡す |
) |
except CalledProcessError as e: |
raise e |
# フレームの情報を抽出 |
frames = json.loads(response.stdout)['frames'] |
# フレームの順番が狂ってしまう場合はソート |
frames = sorted(frames, key=lambda f: f['pkt_pts']) |
return [f['pkt_pts'] for f in frames if f['key_frame']] |
この関数をJupyter Notebookで実行してみたところ、以下のような結果になりました。
11秒と短く、かつ解像度が低めの動画ですが、840msも掛かるんですね…
上記のように、ffprobe
を直接使えばkeyframeを特定できることは分かりました。
しかし、もっと簡単にkeyframeを見つけられるライブラリがないものかと、少し調べてみました。
調査したライブラリと、今回の目的での利用可否は以下の通りです。
名称 | 利用可否 | 備考 |
---|---|---|
opencv-python | × | フレーム自体をNumPy.ndarrayとして取り出すことは可能だが、フレーム単位の情報は取得できない |
PyFFmpeg | × | 3年間メンテナンスをされていない |
moviepy | × | どちらかというと動画編集寄りの機能が多い? |
scikit-video | × | ffprobeコマンドをオプション指定無しで呼び出せる関数はある |
PyAV | ○ | – |
ご覧の通り、今回の目的に適うのはPyAVのみでした。
なので、PyAVを使ってkeyframeのindexを取得してみます。
import av |
def detect_keyframe_with_pyav(filepath): |
video_container = av.open(filepath) |
# decode(video=0): コンテナ内の0番目のvideoチャネルから要素を取り出すiteratorを取得 |
# http://mikeboers.github.io/PyAV/api/container.html?highlight=container#module-av.container |
# http://mikeboers.github.io/PyAV/api/stream.html |
idxs = [] |
for idx, f in enumerate(video_container.decode(video=0)): |
if f.key_frame: |
idxs.append(idx) |
return idxs |
こちらの関数を実行した結果が以下となります。
ffprobe
をsubprocess
経由で呼び出した場合とさほど処理時間に変わりはなく、ffprobe
を利用した場合と同様の結果が得られていることが分かるかと思います。
このように、ffprobe
コマンドをsubprocess
経由で実行する場合よりもシンプルな実装にすることができています。
またkeyframeのindexの取得部分を通常のfor文にしていますが、これはPyAVを利用している場合であれば同時にフレーム自体も取り出せる為、敢えてこうしています。
- keyframeであるか否かの判定のみであれば、PyAVを使うのがシンプルな実装になるのでオススメです。
また、フレームの取り出しを同時に行えると述べましたが、取り出す形式もPIL.ImageやNumPy.ndarrayを指定して取り出せますので、opencv-pythonやPillowといった画像処理ライブラリとの連携もし易いのではないでしょうか。
- IフレームなのかPフレームなのかというレベルでの判定が必要であれば、subprocessを経由してffprobeコマンドを呼び出すことで判定ができます。
また、PyAVでは取得できないような情報を取得したい場合にもこの方法は有用です。