LoginSignup
6
6

More than 1 year has passed since last update.

evision で動画から物体検出する

Last updated at Posted at 2023-01-30

はじめに

以前、 evision と YOLOv3 で画像(静止画)から物体検出するコードを実装したました

今回は動画から物体検出したいと思います

いつものように Livebook で実装していきます

実装したノートブックはこちら

参考記事

実装にあたっては @the_haigo さんの以下の記事を参考にしました

また、 Kino.animate で動画を表示する方法については あんちぽ さんの以下の記事を参考にしました

実行環境

  • Elixir: 1.14.2 OTP 24
  • Livebook: 0.8.1

以下のリポジトリーの Docker コンテナ上で起動しています

Docker が使える環境であれば簡単に実行できます

https://docs.docker.com/engine/install/

Docker Desktop を無償利用できない場合は Rancher Desktop を使ってください

https://rancherdesktop.io/

実行環境の注意点

コンパイル済の evision (OpenCV)では mp4 ファイルを読み込めないため、 evision をソースからコンパイルする必要があります

前述の私のコンテナ(Ubuntu)では、以下のモジュールを追加で apt-get install しました

  • unzip
  • ffmpeg
  • libavcodec-dev
  • libavformat-dev
  • libavutil-dev
  • libswscale-dev

unzip がないとビルド時にエラーが発生します

それ以外は FFmpeg で mp4 などのコーデックを使えるようにするために追加しています

事前準備

何らかの mp4 形式の動画ファイルを /tmp/sample.mp4 に保存しているものとします

セットアップ

Mix.install(
  [
    {:nx, "~> 0.4"},
    {:evision, "~> 0.1"},
    {:req, "~> 0.3"},
    {:kino, "~> 0.8"}
  ],
  system_env: [
    {"EVISION_PREFER_PRECOMPILED", "false"}
  ]
)
  • Nx: 行列演算
  • Evision: 画像処理
  • Req: HTTP リクエスト
  • Kino: Livebook での入出力

環境変数 EVISION_PREFER_PRECOMPILEDfalse を指定することで、 evision をソースからコンパイルするよう指定しています

evision のコンパイルには非常に時間がかかるため、30分程度待ちます

機械学習モデルのロード

今回は Darknet の YOLOv3 を使います

公式で配布している重み、設定ファイル、COCOのラベル一覧をダウンロードします

weights_path = "yolov3.weights"
cfg_path = "yolov3.cfg"
label_path = "label.txt"

"https://pjreddie.com/media/files/yolov3.weights"
|> Req.get!(connect_options: [timeout: 300_000], output: weights_path)

"https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg"
|> Req.get!(output: cfg_path)

"https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names"
|> Req.get!(output: label_path)

Req.get!:output で指定したパスにファイルをダウンロードすることができます

yolov3.weights だけファイルサイズが大きいため、タイムアウト時間を長く指定しています

モデルの読込

Evision.DNN.DetectionModel.detectionModel を使ってモデルを読み込みます

Evision.DNN.DetectionModel.setInputParams で入力パラメータを指定しています

  • scale: 画像ファイルで 0 から 255 になっている色を 0 から 1 に変換するため、 1.0 / 255.0 を指定
  • size: モデルに入力する際に 608 × 608 にリサイズするよう指定
  • swapRB: BGR を RGB に変換するため true を指定
  • crop: リサイズ時に画像を切り抜かないので false を指定
model =
  weights_path
  |> Evision.DNN.DetectionModel.detectionModel(config: cfg_path)
  |> Evision.DNN.DetectionModel.setInputParams(
    scale: 1.0 / 255.0,
    size: {608, 608},
    swapRB: true,
    crop: false
  )

検出結果の番号を名称に変換するため、ラベルの一覧を読み込みます

label_list =
  label_path
  |> File.stream!()
  |> Enum.map(&String.trim/1)

実行結果は以下のような、長さ 80 のリストになります

["person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", "boat",
 "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog",
 "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag",
 "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat",
 "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup",
 "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", ...]

画像の読込

テスト用の画像をダウンロードして読み込みます

img = 
  "https://raw.githubusercontent.com/pjreddie/darknet/master/data/dog.jpg"
  |> Req.get!()
  |> then(& &1.body)
  |> Evision.imdecode(Evision.Constant.cv_IMREAD_COLOR())

dog.png

静止画からの物体検出

Evision.DNN.DetectionModel.detect で物体検出を実行します

  • confThreshold: 検出結果の確信度に対する閾値
  • nmsThreshold: Non-Maximum Suppression で検出した枠が同じ物体だと判断する IoU の閾値

検出結果は {class_ids, scores, boxes} として取得されます

  • class_ids: 物体が何であるかを示す番号のリスト
  • scores: 物体の枠に対する確信度×分類に対する確信度
  • boxes: 物体の位置を示す枠(左上のX座標、Y座標、幅、高さ)

後の処理で使いやすいように score は小数点以下2桁で丸め、 class_id は名称に変換します

predictions =
  model
  |> Evision.DNN.DetectionModel.detect(img, confThreshold: 0.8, nmsThreshold: 0.7)
  |> then(fn {class_ids, scores, boxes} ->
    Enum.zip_with([class_ids, scores, boxes], fn [class_id, score, box] ->
      %{
        box: box,
        score: Float.round(score, 2),
        class: Enum.at(label_list, class_id)
      }
    end)
  end)

実行結果は以下のようになります

[
  %{box: {99, 135, 499, 299}, class: "bicycle", score: 1.0},
  %{box: {478, 81, 211, 86}, class: "truck", score: 0.94},
  %{box: {134, 219, 184, 325}, class: "dog", score: 1.0}
]

自転車とトラックと犬が一つずつ検出できました

静止画への検出結果の書込

検出結果を画像に書き込みます

predictions
|> Enum.reduce(img, fn prediction, drawn_img ->
  {left, top, width, height} = prediction.box

  drawn_img
  # 四角形を描画する
  |> Evision.rectangle(
    {left, top},
    {left + width, top + height},
    {255, 0, 0},
    thickness: 4
  )
  # ラベル文字を書く
  |> Evision.putText(
    prediction.class,
    {left + 6, top + 26},
    Evision.Constant.cv_FONT_HERSHEY_SIMPLEX(),
    0.8,
    {0, 0, 255},
    thickness: 2
  )
end)

dog_detected.png

正しい位置に検出できています

動画の読込

いよいよ動画から物体検出を実行します

Evision.VideoCapture.videoCapture に動画ファイルのパスを指定します

ここでは、"/tmp/sample.mp4" に置いてある動画を使っていますが、お手元の任意の動画を指定してください

input_video = Evision.VideoCapture.videoCapture("/tmp/sample.mp4")

実行結果は以下のようになります

%Evision.VideoCapture{
  fps: 30.0,
  frame_count: 98.0,
  frame_width: 720.0,
  frame_height: 1280.0,
  isOpened: true,
  ref: #Reference<0.1975750727.2452226059.250740>
}

動画ファイルのフレームレート(fps)やフレーム数(frame_count)などが取得できています

もし読み込めていない場合、 fps などの値が 0.0 になり、 isOpenedfalse になります

その場合は FFmpeg のインストールなどが上手くいっていないので、環境構築を見直してください

Evision.VideoCapture.read で動画内のフレームを画像として読み込みます

Evision.VideoCapture.read(input_video)

chair.png

Evision.VideoCapture.get で動画の情報を取得します

Evision.Constant.cv_CAP_PROP_POS_FRAMES を指定すると、現在読込対象になっているフレーム番号を取得します

Evision.VideoCapture.get(input_video, Evision.Constant.cv_CAP_PROP_POS_FRAMES)

実行結果は 1.0 になります

先ほど一回 read したことにより、 0 番のフレームが読み込まれ、次が 1 番になっています

あんちぽさんの記事 で紹介されていた方法で動画を表示します

先頭のフレームから読み込み直すため、対象フレームを 0 に設定してから動かします

動画の最終フレームを過ぎると Evision.VideoCapture.read の結果に false が返り、それによって終了するようにしています

Evision.VideoCapture.set(input_video, Evision.Constant.cv_CAP_PROP_POS_FRAMES, 0)

Kino.animate(1, 0, fn _, frame_index ->
  frame = Evision.VideoCapture.read(input_video)

  if frame do
    {:cont, frame, frame_index + 1}
  else
    :halt
  end
end)

chair.gif

ゆっくり動画が再生されました

全フレームを読み込んで表示しているため、どうしても元より遅くなります

動画からの物体検出

静止画で行った物体検出を関数にしておきます

detect = fn img, model ->
  model
  |> Evision.DNN.DetectionModel.detect(img, confThreshold: 0.8, nmsThreshold: 0.7)
  |> then(fn {class_ids, scores, boxes} ->
    Enum.zip_with([class_ids, scores, boxes], fn [class_id, score, box] ->
      %{
        box: box,
        score: Float.round(score, 2),
        class: Enum.at(label_list, class_id)
      }
    end)
  end)
  |> Enum.reduce(img, fn prediction, drawn_img ->
    {left, top, width, height} = prediction.box

    drawn_img
    |> Evision.rectangle(
      {left, top},
      {left + width, top + height},
      {255, 0, 0},
      thickness: 8
    )
    |> Evision.putText(
      "#{prediction.class} #{prediction.score}",
      {left + 6, top + 52},
      Evision.Constant.cv_FONT_HERSHEY_SIMPLEX(),
      1.6,
      {0, 0, 255},
      thickness: 4
    )
  end)
end

全フレームに対して検出を実行します

Evision.VideoCapture.set(input_video, Evision.Constant.cv_CAP_PROP_POS_FRAMES, 0)

Kino.animate(100, 0, fn _, frame_index ->
  frame = Evision.VideoCapture.read(input_video)

  if frame do
    {:cont, detect.(frame, model), frame_index + 1}
  else
    :halt
  end
end)

chair_detected.gif

フレーム毎に物体検出結果が書き込まれました

動画への検出結果の書込

検出結果を動画ファイルとして保存します

mp4 形式で保存するように Evision.VideoWriter.fourcc でコーデックを指定します

fourcc = Evision.VideoWriter.fourcc(hd('m'), hd('p'), hd('4'), hd('v'))

Evision.VideoWriter.videoWriter で出力の設定をします

フレームレートや画像の縦横サイズを指定します

output_video =
  Evision.VideoWriter.videoWriter(
    "/tmp/sample_detected.mp4",
    fourcc,
    input_video.fps,
    {
      trunc(input_video.frame_width),
      trunc(input_video.frame_height)
    }
  )

再びフレームを 0 まで戻して、全フレームに対して物体検出を実行します

今回は Livebook 上に結果を表示しないで動画ファイルに書き込みます

Evision.VideoCapture.set(input_video, Evision.Constant.cv_CAP_PROP_POS_FRAMES, 0)

0
|> Stream.iterate(&(&1 + 1))
|> Stream.map(fn frame_index ->
  IO.inspect("#{frame_index}/#{trunc(input_video.frame_count)}")
  input_frame = Evision.VideoCapture.read(input_video)
  if input_frame do
    output_frame = detect.(input_frame, model)
    Evision.VideoWriter.write(output_video, output_frame)
    true
  else
    false
  end
end)
|> Stream.take_while(& &1)
|> Enum.to_list()

実行すると以下のように経過が表示されていきます

"0/98"
"1/98"
"2/98"
...

最終的にフレーム数分の true が入ったリストが返ります

全フレーム処理し終わったらファイルを解放します

Evision.VideoWriter.release(output_video)

検出結果の確認

ちゃんと書き込めたか確認しましょう

detected_video = Evision.VideoCapture.videoCapture("/tmp/sample_detected.mp4")

Kino.animate(1, 0, fn _, frame_index ->
  frame = Evision.VideoCapture.read(detected_video)

  if frame do
    {:cont, frame, frame_index + 1}
  else
    :halt
  end
end)

chair_detected_writed.gif

ちゃんと全フレーム書き込めていました

まとめ

Python + OpenCV と同じ要領で Elixir + evision で動画処理できました

CPU 環境では遅いので、 GPU でどれだけ速くなるかも検証してみます

6
6
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6