こしあん
2023-02-05

ディープラーニングの動画読み込みをいい感じにしてくれる「Decord」の紹介


2.6k{icon} {views}


ディープラーニングでの動画解析向けの読み込みライブラリ、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

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.

https://github.com/dmlc/decord/blob/d2e56190286ae394032a8141885f76d5372bd44b/python/decord/video_loader.py

(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の中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


One Comment

Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です