OpenCVとYOLOv5を使って動画切り抜きをしてみる

## はじめに
自動で動画切り抜きする方法を、クラウドを使わずにローカル環境で試してみた。
検知能力もそこそこ優秀で比較的処理が軽いと言われる`YOLOv5`と OpenCV を使って作ってみたので、メモとして残しておく。

## 実施環境
■ PCスペック
　CPU：Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz   3.20 GHz
　ﾒﾓﾘ：8.00 GB 

■ OS
　Windows 10 Pro　

■ 各種SW＆パッケージのバージョン（今回インストールするもの含む）
　python 3.9.5
　pip 21.3.1
　opencv-python 4.5.4.60
　YOLOv5

## メモ内容
①環境準備の流れと、②Pythonコードをメモとして残す。

### ① 環境準備
色々なパッケージが必要となりローカル環境が散らかりそうだったため、自分は仮想環境上で環境を構築している。（手順は下記を参照）

https://qiita.com/smiler5617/items/2abce37f9684394562e3

https://qiita.com/smiler5617/items/e0d9b3034d79457cc253

**パッケージのインストールの前に`YoLov5`のインストールに必要なソースをダウンロードする**
　※clone先は仮想環境(venv)で作成した場所を指定

```shell:
git clone https://github.com/ultralytics/yolov5
```


**必要パッケージのインストール**

仮想環境を`Activate`にした状態で以下を実行。

```shell:
pip install --upgrade pip

# パッケージのインストールコマンド
pip install
pip install datetime
pip install numpy pandas
pip install opencv-python

# 物体検出のための学習済みモデル利用に使うパッケージ一覧
pip install -r ./yolov5/requirements.txt
```

**Pythonコマンドで`import cv2` や `import torch`が実行できれば、一旦インストール完了と考えてOK**

```shell:
(opencv) C:\Users\*******\*******>python  ## opencvという名前の仮想環境を作っているため(opencv)となっている。
Python 3.9.5 (tags/v3.9.5:0a7dcbd, May  3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>> import torch
```

**`YOLOv5`の動作確認**
ターミナルから動かす場合は以下のコマンドを実行する。

```shell:
cd yolov5
python detect.py --source ./data/images/ --weights yolov5s.pt --conf 0.4
```

上記コマンドの引数の説明。

　source：画像のフォルダ、または画像のパスを指定。
　　　　　※`./data/images` にサンプル画像が入っている。
　　conf：指定した値以下の確率値は表示しない。
 weights：物体検出時に利用する事前学習済みモデルの重みファイルを指定。
　　　　　※モデルが存在しない初回時は`yolov5s.pt`がダウンロードされる。。

実行後にフレームとラベル付きの画像ファイルが`runs/detect/exp`（デフォルト）に保存される。

※1)．以下の様なオプションを付けることで、画像は保存せず、結果をテキストで出力すること可能。
　・`--save-txt`　　推論結果(検出座標と予測クラス)をtxtファイルに出力する
　・`--save-conf`　　推論結果(クラスの確率)をtxtファイルに出力する
　・`--nosave` 　　画像を保存しない
　　**実行対象を動画ファイルにした場合、全てのフレームに対して画像検出処理がされる**

※2)．YOLOv5で検知できる物体の種類
`person` `bicycle` `car` `motorcycle` `airplane` `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` `broccoli` `carrot` `hot dog` `pizza` `donut` `cake` `chair` `couch` `potted plant` `bed` `dining table` `toilet` `tv` `laptop` `mouse` `remote` `keyboard` `cell phone` `microwave` `oven` `toaster` `sink` `refrigerator` `book` `clock` `vase` `scissors` `teddy bear` `hair drier` `toothbrus`
　物体種類の指定の時は、クラス番号で指定する必要がある。詳しくは `./data/coco.yaml` を見れば分かる。

### ② 実現するためのPython サンプルコードを紹介

今回は **『動画の中で鳥が映っている部分のみを自動で切り抜いてくれるプログラム』** を作ってみる。

サンプル動画は、**森の中で一匹の鳥が見え隠れするような動画** を選定。

※以下の動画サンプルサイトから良さそうなものをダウンロードしました。
　https://pixabay.com/ja/videos/
　https://www.pexels.com/ja-jp/videos/
　https://www.motionelements.com/ja/free/stock-footage

以下の STEP で機能を作って実現していく。
　**[STEP１] 動画のフレーム数を減らして短くする処理。**
　**[STEP２] 各フレームに対して物体検出を行い、鳥が映っているフレームのみ繋ぎ合わせる。**


#### [STEP１] 動画のフレーム数を減らして短くする。
動画ファイルでそのまま物体検出を行うと、全てのフレームに対して検知処理が走ってしまい、必要以上の計算コストかかってしまう。
実際には 数秒間隔くらいで検出を行えれば十分なので、元の動画から数秒間隔のフレームだけで動画を繋ぎ合わす加工を行う。

```py: 動画のフレーム数を減らして短くする。
# パッケージインポート
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import cv2

###################################################################
# 指定した "秒" 間隔のみのフレームに絞り、動画ファイルを生成。
###################################################################

### 引数の説明
# path_in：元動画ファイルのパス
# path_out：出力動画ファイルのパス
# interval：抽出するフレームの間隔（秒）

def frame_out(file_in, file_out, interval):
    
    #
    movie= cv2.VideoCapture(file_in)
    
    # 元画像の画像サイズを取得
    w = int(movie.get(cv2.CAP_PROP_FRAME_WIDTH))
    h = int(movie.get(cv2.CAP_PROP_FRAME_HEIGHT))
    print(w, ' × ', h)

    # 元画像のfps 1秒あたりのフレーム数を取得
    fps = int(movie.get(cv2.CAP_PROP_FPS))
    
    # 再生時間と、抽出するフレームの間隔から、フレーム数を取得。
    end_frame = int(movie.get(cv2.CAP_PROP_FRAME_COUNT)/(movie.get(cv2.CAP_PROP_FPS) * interval))
    print('フレーム数：', end_frame)
        
    # 出力動画ファイルの生成
    fourcc = cv2.VideoWriter_fourcc('m','p','4', 'v')
    video = cv2.VideoWriter(file_out, fourcc, 1, (w, h))
    
    for sec in range(end_frame):
        
        # 取得するフレーム番号を指定
        pos_frame = int(fps * (sec + 1) * interval)        
        movie.set(cv2.CAP_PROP_POS_FRAMES, pos_frame)
        
        # フレームを読みだす。
        ret, frame = movie.read()
        if not ret:
            break
        video.write(frame)
            
    # 出力動画ファイルの解放
    video.release()

# 上記関数の実行処理（サンプル）
source_file = './サンプル動画_org.mp4'
output_file = './処理後動画.mp4'
interval = 1
frame_out(source_file, output_file, interval)
```

#### [STEP２] 各フレームに対して物体検出を行い、鳥が映っているフレームのみ繋ぎ合わせる。　　　
上記の[STEP1] を少し改良し、動画ファイルから抽出したフレームに対して物体検出を行い、**"鳥"** が映っているか判定する。
映っていた場合、生成動画に繋ぎ合わせる。

```py:各フレームに対して物体検出を行い、鳥が映っているフレームのみ繋ぎ合わせる
# パッケージインポート
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch    # モデルを呼び出すために追加
import cv2

###################################################################
# 指定した "秒" 間隔のみのフレームに絞り、動画ファイルを生成。
###################################################################

### 引数の説明
# path_in：元動画ファイルのパス
# path_out：出力動画ファイルのパス
# interval：抽出するフレームの間隔（秒）
# 　　mdl：物体検知に使う学習済みモデル
# 　label：検知したい物体
# 　conf_border：検知したい物体の確率閾値

def frame_extract_withlabel(file_in, file_out, interval, mdl, label, conf_border):
    
    # 物体検出に使用するモデルの読み込み & 設定
    model = torch.hub.load('ultralytics/yolov5', mdl)
    model.classes = label
    model.conf = conf_border
    
    # 動画の読み込み
    movie= cv2.VideoCapture(file_in)
    
    # 元画像の画像サイズを取得
    w = int(movie.get(cv2.CAP_PROP_FRAME_WIDTH))
    h = int(movie.get(cv2.CAP_PROP_FRAME_HEIGHT))
    print(w, ' × ', h)

    # 元画像のfps 1秒あたりのフレーム数を取得
    fps = int(movie.get(cv2.CAP_PROP_FPS))
    
    # 再生時間と、抽出するフレームの間隔から、フレーム数を取得。
    end_frame = int(movie.get(cv2.CAP_PROP_FRAME_COUNT)/(movie.get(cv2.CAP_PROP_FPS) * interval))
    print('フレーム数：', end_frame)
        
    # 出力動画ファイルの生成
    fourcc = cv2.VideoWriter_fourcc('m','p','4', 'v')
    video = cv2.VideoWriter(file_out, fourcc, 1, (w, h))
    
    for sec in range(end_frame):
        
        # 取得するフレーム番号を指定
        pos_frame = int(fps * (sec + 1) * interval)        
        movie.set(cv2.CAP_PROP_POS_FRAMES, pos_frame)
        
        # フレームを読みだす。
        ret, frame = movie.read()
        
        if not ret:
            break
        
        ### 読み出されたフレーム毎に物体検知を行う
        results = model(frame)
        if len(results.pandas().xywh[0]) >0 :
            # 1～4カラムまでの物体位置を表す表現の仕方が異なる
            #display(results.pandas().xyxy[0])    # 検知した物体の square の4点の座標
            display(results.pandas().xywh[0])    #検知した物体の 中心X・Y座標 & square の高さと幅
            video.write(frame)
            
    # 出力動画ファイルの解放
    video.release()


# 上記関数の実行処理
source_file = './サンプル動画_自然_鳥.mp4'
output_file = './処理後動画.mp4'
interval = 0.5
mdl_name = 'yolov5x'
target_label = 14    # 鳥は "14" のため

frame_extract_withlabel(source_file, output_file, interval, mdl_name, target_label, 0.3)
```

上記pythonコードをを実行すれば、鳥だけが映っている動画を自動生成できる。