ディープラーニングの動画読み込みをいい感じにしてくれる「Decord」の紹介
ディープラーニングでの動画解析向けの読み込みライブラリ、Decordを紹介します。OpenCVよりもフレーム間のスキップやバッチ化が簡単にできるようになっています。PyTorchと連携させることも可能です。
目次
はじめに
動画のディープラーニング、前処理を書くのがとても大変ですよね。手軽に使えるのはOpenCVですが、VideoCaptureがシーケンシャルリードしかできないので、それでできるのも限度がある。なんかいい感じでやってくれるライブラリがほしいです。
とある論文のコードを見ていたら、前処理でDecordというライブラリを使っていました。これが便利だったので、紹介していきたいと思います。
使い方
公式ドキュメントはこちら
https://github.com/dmlc/decord#usage
ここである程度まとまっていますが、以下かいつまんで紹介していきます。
インストール
pipからインストールできます
pip install decord
VideoReaderとVideoLoader
Decordによる動画ファイルの読み込みは、VideoReaderとVideoLoaderがあります。それぞれ次のような使い分けをします。
- VideoReader:単一の動画ファイルの読み込み
- VideoLoader:複数の動画ファイルや動画データセットの読み込み。PyTorchのDataLoaderに近い
データセット例
この記事では、例としてInstructionVideosデータセットの読み込みを行います。ダウンロードして解凍すると、以下のようなディレクトリ構成で動画が展開されます。
- data_new
- data_release
- changing_tire
- annot
- subtitles
- videos
- chaning_tire_001.mpg
- chaning_tire_002.mpg
- ………
- coffee
- annot
- subtitles
- videos
- coffee_0001.mpg
- …
- cpr
- …
- jump_car
- …
- repot
- …
- changing_tire
- data_release
VideoReader
まずはVideoReaderで単一ファイルを読み込みます。
データを読み込んでフレーム数を表示するのは以下の通り。
import decord
with open("data_new/data_release/changing_tire/videos/changing_tire_0001.mpg", "rb") as fp:
vr = decord.VideoReader(fp, ctx=decord.cpu(0))
print(len(vr)) # 3895
ctxの部分にはGPUを指定できますが、ここでは省略します。全フレームをシーケンシャルリードして配列に格納するのは以下の通りです。
import decord
import numpy as np
with open("data_new/data_release/changing_tire/videos/changing_tire_0001.mpg", "rb") as fp:
vr = decord.VideoReader(fp, ctx=decord.cpu(0))
frames = vr.get_batch(np.arange(len(vr)))
print(frames.shape) # (3985, 356, 480, 3)
注意点としては、Decordのデータ型はdecord.ndarray.NDArrayというNumPy配列と似て非なる型なので、NumPy配列に変換したければ、asnumpy()を挟む必要があります。
import decord
import numpy as np
import matplotlib.pyplot as plt
with open("data_new/data_release/changing_tire/videos/changing_tire_0001.mpg", "rb") as fp:
vr = decord.VideoReader(fp, ctx=decord.cpu(0))
frames = vr.get_batch(np.arange(len(vr)))
# uint8 <class 'decord.ndarray.NDArray'>
print(frames.dtype, type(frames))
# TypeError: 'NDArray' object is not subscriptable
#plt.imshow(frames[0])
plt.imshow(frames.asnumpy()[0]) # これはOK
plt.show()
get_batchの中身は任意のフレームインデックスを設定できるので、「10フレーム飛ばしで読んで」なども余裕です。
with open("data_new/data_release/changing_tire/videos/changing_tire_0001.mpg", "rb") as fp:
vr = decord.VideoReader(fp, ctx=decord.cpu(0))
frames = vr.get_batch(np.arange(0, len(vr), 10))
print(frames.shape) # (399, 356, 480, 3)
同様にして、任意のフレームインデックスから開始も簡単です。
VideoLoader
VideoLoaderの返り値の解釈
VideoReaderだけでも十分使えますが、ディープラーニングの動画データセットの場合、こちらがメインではないでしょうか。複数ファイルを読み込む場合を想定します。
import decord
import numpy as np
import glob
files = sorted(glob.glob("data_new/data_release/changing_tire/videos/*"))
loader = decord.VideoLoader(files,
ctx=decord.cpu(0),
shape=(2, 320, 240, 3), interval=1, skip=5, shuffle=0)
for batch in loader:
print(batch[0].shape)
print(batch[1])
まずVideoLoaderの引数はさておき、バッチの中身を見ていきます。バッチの中身は2つの変数のTupleからなり、
(2, 320, 240, 3)
[[0 0]
[0 2]]
最初のは画像テンソルで、VideoReaderと同様です。VideoLoaderの引数shapeで指定したものが返ってきます。2つ目の配列ですが、この意味はソースに書いてあります。
ndarray, ndarray Frame data and corresponding indices in videos. Indices are [(n0, k0), (n1, k1)...] where n0 is the index of video, k0 is the index of frame in video n0.
(2, 320, 240, 3)のテンソルの1番目、2番目の要素について、「動画ファイルのインデックス、フレームのインデックス」を示します。先程の例なら、
- 0番目のファイルの0番目のフレーム
- 0番目のファイルの2番目のフレーム
です。特にファイルのインデックスは、動画に紐づくアノテーションを取得するときに必須になるでしょう。
VideoLoaderのshapeが(3, 320, 240, 3)なら、3×2配列になります。
(3, 320, 240, 3)
[[0 0]
[0 2]
[0 4]]
VideoLoaderの引数
次にVideoLoaderの引数を見ていきます。全部必須です。分かりづらいのがintervalとskipとshuffleでしょうか。Docstringからです。
Parameters
----------
uris : list of str
List of video paths.
ctx : decord.Context or list of Context
The context to decode the video file, can be decord.cpu() or decord.gpu().
If ctx is a list, videos will be evenly split over many ctxs.
shape : tuple
Returned shape of the batch images, e.g., (2, 320, 240, 3) as (Batch, H, W, 3)
interval : int
Intra-batch frame interval.
skip : int
Inter-batch frame interval.
shuffle : int
Shuffling strategy. Can be
`0`: all sequential, no seeking, following initial filename order
`1`: random filename order, no random access for each video, very efficient
`2`: random order
`3`: random frame access in each video only.
intervalがバッチ内の差分、skipがバッチ間の差分です。具体的に見てみましょう。
skip=0, interval=0の場合
loader = decord.VideoLoader(files,
ctx=decord.cpu(0),
shape=(3, 320, 240, 3), interval=0, skip=0, shuffle=0)
for i, batch in enumerate(loader):
print(batch[1])
if i == 2:
break
#[[0 0]
# [0 1]
# [0 2]]
#[[0 3]
# [0 4]
# [0 5]]
#[[0 6]
# [0 7]
# [0 8]]
skip=0, interval=1の場合
#[[0 0]
# [0 2]
# [0 4]]
#[[0 5]
# [0 7]
# [0 9]]
#[[ 0 10]
# [ 0 12]
# [ 0 14]]
skip=1, interval=0の場合
#[[0 0]
# [0 1]
# [0 2]]
#[[0 4]
# [0 5]
# [0 6]]
#[[ 0 8]
# [ 0 9]
# [ 0 10]]
なんとなくわかってきたのではないでしょうか。
- interval : 抽出フレームが切り替わるときに足される
- skip :バッチが切り替わるときに足される
という挙動になるようです。
shuffleの挙動について
コメントを読めばわかるのですが、シャッフルありの場合1を使うことが多いのではないかと思います。ここで、フレーム数が異なる動画が、どういう挙動をして読み込まれるのか興味が湧きます。以下のような実験をしてみます。
files = sorted(glob.glob("data_new/data_release/changing_tire/videos/*")[:3])
loader = decord.VideoLoader(files,
ctx=decord.cpu(0),
shape=(3, 320, 240, 3), interval=500, skip=0, shuffle=1)
for batch in loader:
print(batch[1])
[[ 1 0]
[ 1 501]
[ 1 1002]]
[[ 0 0]
[ 0 501]
[ 0 1002]]
[[ 2 0]
[ 2 501]
[ 2 1002]]
[[ 0 1003]
[ 0 1504]
[ 0 2005]]
[[ 0 2006]
[ 0 2507]
[ 0 3008]]
[[ 1 1003]
[ 1 1504]
[ 1 2005]]
[[ 2 1003]
[ 2 1504]
[ 2 2005]]
[[ 1 2006]
[ 1 2507]
[ 1 3008]]
この結果を見ると以下のような推測ができます。
- 動画の読み込みは、異なるスレッド or プロセスが並列処理で動いている
- shuffle=1はそれぞれのスレッド、プロセスが並列が動いているゆえに、読み込みの順番が保証されなく、シャッフルしているように見える
この設計はディープラーニングと相性がよくて、動画モデルの場合は、VideoLoaderのような(B, H, W, C)のようなテンソルにはせず、(N, B, H, W, C)のような5階テンソルにすることが多いです(3DCNNのような場合)。4階テンソルだとバッチサイズ2以上で、フレーム間の演算ができなくなってしまうのですよね。
実践的には、VideoLoaderは(モデルにとっての)単体サンプルの読み込みで使って、それをStackなりすることでDataLoaderとするのような設計になるのではないかと思います。
PyTorchをフックさせる
PyTorchのフックは非常に簡単で、
decord.bridge.set_bridge('torch')
これを最初に追加するだけです。実際に以下のようにすると、バッチの型がPyTorchのテンソルになります。これ便利ですね。
files = sorted(glob.glob("data_new/data_release/changing_tire/videos/*"))
decord.bridge.set_bridge('torch')
loader = decord.VideoLoader(files,
ctx=decord.cpu(0),
shape=(3, 320, 240, 3), interval=1000, skip=0, shuffle=1)
for batch in loader:
print(batch[1], batch[1].dtype)
print(batch[0].dtype, batch[0].dtype)
break
#tensor([[ 8, 0],
# [ 8, 1001],
# [ 8, 2002]]) torch.int64
#torch.uint8 torch.uint8
まとめ
全部は紹介しきれなかったのですが、Decordを使うと動画の前処理(読み込み)をいい感じにやってくれそうです。他にも動画と音声を同時に読み込む(AVReader)などもあるので、ぜひ活用してみたいです。
追記
Twitterで、長時間の動画(2~3時間の動画複数本)に対してDecord使うとOut of memory発生してプロセス強制終了される報告をいただきました。その方はOpenCVのVideoReaderで書き直したそうです。そういうハマりどころあるんのか
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー