Pythonで動画のkeyframeのindexを取得してみた

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

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


1. 環境、データ

  • 環境

  • データ
    検証に使わせていただいたデータは、Pixabayにて公開されているこちらの動画になります。
    こちらのファイルをFireworks – 348.mp4として使いました。

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

まずはffprobeコマンドで、動画内の全フレームの情報を取得してみます。

これで、output_with_cli.jsonに動画内の全フレームの情報が書き出されます。
以下は出力されたjsonの一部となります。

json内のkey_frameキーを見ると、そのフレームがkeyframeか否かが判別できます。
また、keyframeか否かだけではなく、フレームの種別迄確認する際にはpict_typeキーで確認できます。

※ 取得できる情報に関して

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を使います。

この関数を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を取得してみます。

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

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

まとめ

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