こんにちは。技術研究所の910です。
先日弊社にて開催いたしましたオープンハウスでは、動画を活用した研究に関する発表をさせていただきました。

 

なので今回は動画にフォーカスを当てて、動画に含まれるkeyframeのindexの取得を試してみました。
keyframeについてはこちらの記事に分かり易くまとめてくださっています。

1. 環境、データ

  • 環境
(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として使いました。

2. ffprobeで動画内の全フレームの情報を取得

まずは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の話:音ずれ問題や時間が変になるときのために ヽ(゚ー゚ヽ)(ノ゚ー゚)ノわぁい

3. ffprobeをPythonから使ってkeyframeのindexを取得

全フレームの情報を取得する為のコマンドは分かったので、この中から必要な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も掛かるんですね…

4. Pythonの動画処理ライブラリでkeyframeのindexを取得

上記のように、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

こちらの関数を実行した結果が以下となります。
ffprobesubprocess経由で呼び出した場合とさほど処理時間に変わりはなく、ffprobeを利用した場合と同様の結果が得られていることが分かるかと思います。

このように、ffprobeコマンドをsubprocess経由で実行する場合よりもシンプルな実装にすることができています。
またkeyframeのindexの取得部分を通常のfor文にしていますが、これはPyAVを利用している場合であれば同時にフレーム自体も取り出せる為、敢えてこうしています。

まとめ

  • keyframeであるか否かの判定のみであれば、PyAVを使うのがシンプルな実装になるのでオススメです。
    また、フレームの取り出しを同時に行えると述べましたが、取り出す形式もPIL.ImageやNumPy.ndarrayを指定して取り出せますので、opencv-pythonやPillowといった画像処理ライブラリとの連携もし易いのではないでしょうか。
  • IフレームなのかPフレームなのかというレベルでの判定が必要であれば、subprocessを経由してffprobeコマンドを呼び出すことで判定ができます。
    また、PyAVでは取得できないような情報を取得したい場合にもこの方法は有用です。