MacでPython+OpenCVを利用した動画処理

Mac上のOpenCVで動画処理のプログラムを作成する際に、いくつかハマった点があったので、その備忘録です。

# 初めに
作成したプログラムは、入力した動画の、1フレームごとの画像に対して任意の画像処理を行い、処理後の画像を再び動画として再構成して保存するもの。動画処理の流れは、以下のようになっている。

1. ローカルに保存されている動画を読み込む
2. 1フレームずつ画像処理
3. 動画ファイルとして書き込み


## 実行環境
- macOS Catalina(ver 10.15.3)
- python 3.8

opencv-pythonのインストールは、以下コマンドで行う。

```sh
pip install opencv-python
```


## ソースコードの全体像
作成したソースコードの全体像は以下の通り。動画に対する処理は、67行目の`processed_frame = cv2.flip(frame, -1)`にある、上下左右の反転を行うものをサンプルとして行っている。こちらのプログラムを実行することで、入力した動画を上下左右反転させた動画を作成できる。

上下左右の反転処理の箇所を、任意の画像処理に置き換えることで、Pythonで様々な動画処理ができるようになる。

```python:./main.py
import os
import cv2
import argparse


class MovieIter(object):
    """
    動画のIterator
    """

    def __init__(self, moviefile, inter_method=cv2.INTER_AREA):
        if os.path.isfile(moviefile):  # mp4ファイルが存在するとき
            print("[Loading]\tLoading Movie")
            self.org = cv2.VideoCapture(moviefile)

        self.framecnt = 0
        self.inter_method = inter_method

    def __iter__(self):
        return self

    def __next__(self):
        self.end_flg, self.frame = self.org.read()
        if not self.end_flg:  # 動画の最後に到達したらループを終了する
            raise StopIteration()
        self.framecnt += 1
        return self.frame

    def __del__(self):
        self.org.release()

    def get_size(self) -> (int, int):
        """動画の解像度を返す

        Returns:
            (int, int):　(w, h)
        """
        return (
            int(self.org.get(cv2.CAP_PROP_FRAME_WIDTH)),
            int(self.org.get(cv2.CAP_PROP_FRAME_HEIGHT)),
        )

    def get_fps(self) -> float:
        """動画のFpsを返す

        Returns:
            float: fps
        """
        return self.org.get(cv2.CAP_PROP_FPS)


def main(movie_path, save_path):
    # 入力する動画を定義
    input_movie = MovieIter(movie_path)
    input_moveie_size = input_movie.get_size()

    # 出力する動画ファイルの定義
    fourcc = cv2.VideoWriter_fourcc(*"mp4v")
    output_movie = cv2.VideoWriter(
        os.path.join(save_path, "output.mp4"),  # 保存するパス
        fourcc,  # コ-ディック
        input_movie.get_fps(),  # 動画のfps
        input_moveie_size,  # 動画の解像度
    )

    for frame in input_movie:
        processed_frame = cv2.flip(frame, -1)  # 上下左右反転

        # 動画の解像度が異なると保存できないので確認する
        assert processed_frame.shape[:2][::-1] == input_moveie_size
        output_movie.write(processed_frame)

    # リリースして動画を保存
    output_movie.release()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="動画を処理するプウログラム")

    parser.add_argument("movie_path", help="処理を行う動画のURL")
    parser.add_argument("--save_path", default="results", help="生成した画像を保存するディレクトリのパス")

    args = parser.parse_args()
    main(args.movie_path, args.save_path)
```

実行時のコマンドは以下の通り。処理に成功すると、`${処理する動画のパス}`の上下左右が反転した動画が作成され、`--save_path`に指定したフォルダに保存される。

```sh
python main.py ${処理する動画のパス} --save_path ${処理後の動画を保存するパス}
```


# ハマったところ
## 動画に書き込む画像サイズの指定
ハマりポイントの1つ目は、ソースコード57行目の`cv2.VideoWriter()`に指定する画像のサイズと、71行目の`output_movie.write(processed_frame)`で書き込む画像のサイズが異なると、動画の書き出しに失敗する点である。


```python
~~~省略~~~~
    output_movie = cv2.VideoWriter(
        os.path.join(save_path, "output.mp4"),  # 保存するパス
        fourcc,  # コ-ディック
        input_movie.get_fps(),  # 動画のfps
        input_moveie_size,  # 動画の解像度  ← ココ！
    )
~~~省略~~~~
```

一般的にopencvの画像サイズは、`(height, width, chnnel)`の順で定義されている。しかし一方で`cv2.VideoWriter()`で指定する画像のサイズは`(width, height)`の順で定義する必要がある。なので、普段のノリで「画像サイズは`(height, width)`の順だね〜」と思って指定すると、定義時と書き込み時の画像サイズの違いにより、動画の書き出しに失敗する現象が発生する。

しかも困ることに、上記のような、サイズの違いによる動画の書き出しに失敗する現象が起きても、opencvはエラーやワーニングを返してこない。それにより、「プログラムは正常なのに、なぜ保存できないんだろう..」と無駄に時間を浪費してしまい、画像サイズの指定ミスに気づくのにかなり時間がかかってしまった。

解決策として、以下のようにassertで確認する処理を入れておいた。

```python
~~~省略~~~~
        # 動画の解像度が異なると保存できないので確認する
        assert processed_frame.shape[:2][::-1] == input_moveie_size
~~~省略~~~~

```

### グレースケールの動画を作成する場合
作成する動画の形式がグレースケールの場合は、`cv2.VideoWriter()`の5つ目の引数に、0を指定する必要がある。これも指定し忘れると、上記の件と同様に、エラー・ワーニングなしで、ただ動画の書き出しに失敗する現象が起きる。


```python
~~~省略~~~~
    output_movie = cv2.VideoWriter(
        os.path.join(save_path, "output.mp4"),  # 保存するパス
        fourcc,  # コ-ディック
        input_movie.get_fps(),  # 動画のfps
        input_moveie_size,  # 動画の解像度  ← ココ！
        0 # ← ココ！
    )
~~~省略~~~~
```

## 動画のコーデックの指定
2つ目は、ソースコード58行目の動画のコーデック形式に、実行環境に応じた対応している形式を指定する必要がある点である。

```python
~~~省略~~~~
    # 出力する動画ファイルの定義
    fourcc = cv2.VideoWriter_fourcc(*"mp4v") # ← ココ！
~~~省略~~~~
```


「opencv python 動画」などで検索して上位に出てくる記事だと、`cv2.VideoWriter_fourcc(*'XVID')`と指定されていることが多い。その為初めは、それに倣ってそのまま`.avi`形式で保存を試したが、その形式での出力がどうやら筆者のMacに対応しておらず、うまく書き出しが行われなかった。バージョンによっては、MacOS上で`.avi`形式で保存することもできるようだが、`mp4`形式で保存をするのが最も安定すると思われる。

Mac上での各形式の動作状況が、有志により以下のリンクにまとめられている。プログラムを書く前に、自分の使いたい形式がMac上でうまく動作するかを確認した方が良い。
- [OpenCV Video Writer on Mac OS X · GitHub](https://gist.github.com/takuma7/44f9ecb028ff00e2132e)

# 終わりに
形式違いやサイズ違いで保存に失敗してるなら、ワーニングくらい出して欲しいですね...

# 参考
- [Python+OpenCVでアニメのカット検出 - にせねこメモ](https://nixeneko.hatenablog.com/entry/2017/09/07/000000)
- [[基礎] python + openCV で動画を扱う - Qiita](https://qiita.com/ANNEX_IBS/items/6dbd02ff89a4a2de0b3b)
- [python - Save grayscale video in OpenCV? - Stack Overflow](https://stackoverflow.com/questions/50037063/save-grayscale-video-in-opencv)