Python 画像から動画を作成する(マルチプロセス)

# はじめに

今回はフォルダ内の画像ファイルから動画を作成します。
これはOpenCVを使って簡単に実現できます。

```python:test_img_cv.py
import os
import glob
from tqdm import tqdm
import cv2

input_dir   = 'image'           # 入力する画像が保存されたフォルダ名
output_path = input_dir + '_cv' + '.mp4'    # 作成する 動画ファイル
print(output_path)

# フォルダ内の画像のファイルリストを取得する
files  = glob.glob(os.path.join(input_dir, '*.png'))
files += glob.glob(os.path.join(input_dir, '*.jpg'))
files.sort()
frames=len(files)
assert frames != 0, 'not found image file'    # 画像ファイルが見つからない

# 最初の画像の情報を取得する
img = cv2.imread(files[0])
h, w, channels = img.shape[:3]

# 作成する動画
codec = cv2.VideoWriter_fourcc(*'mp4v')
#codec = cv2.VideoWriter_fourcc(*'avc1')
writer = cv2.VideoWriter(output_path, codec, 30000/1001, (w, h),1)

bar = tqdm(total=frames, dynamic_ncols=True)
for f in files:
    # 画像を1枚ずつ読み込んで 動画へ出力する
    img = cv2.imread(f)
    writer.write(img)   
    bar.update(1)
    
bar.close()
writer.release()
```


これをマルチスレッド化してもっと早くしたいと思いましたが、Pythonは「マルチスレッドでプログラミングできるけど、実際の動作は1スレッドずつしか動かない」とのことで マルチスレッド化では早くならないようです。

https://qiita.com/Jungle-King/items/1d332a91647a3d996b82

そこでマルチプロセス化となるのですが、FFStreamはもともと別プロセスでffmpeg.exeを使うようになっているのでOpenCVの例をFFStreamを使って置き換えれば簡単にマルチプロセス化できます。

ということで今回はFFStreamを使って簡単に（マルチスレッドの様に）複数の画像を同時に読み込む方法を紹介します。


# 環境
- Windows
- Python v3.11.4

# ソフトウエア
次のものが必要です。
- FFStream v1.3.0 [ダウンロードページ](https://qiita.com/amaman/items/463ddedf52393d3e0678)
- ffmpeg-python==0.2.0
- ffmpeg
    ffmpeg-2020-12-20-git-ab6a56773f-full_build.zip [ダウンロードページ](https://github.com/GyanD/codexffmpeg/releases/tag/2020-12-20-git-ab6a56773f)

ffstream.zipを解凍してできる'FFStream'フォルダをプロジェクトへコピー。
ffmpeg-2020-12-20-git-ab6a56773f-full_build.zipを解凍してできる'ffmpeg.exe'と'ffprobe.exe'をプロジェクトへコピー。
ffmpeg-pythonはpipでPythonへインストールします。
# テスト用の連番画像を作成する
コマンドライン等でffmpeg.exeを使って動画からテスト用の連番画像を作成します。

```python
>ffmpeg -i test.mp4 -vframes 1000 image/%04d.png
```
プロジェクト内にimageフォルダを作成してから上記を実行すると imageフォルダ内にtest.mp4の先頭1000フレームの画像ファイルが作成されます。


# 画像から動画を作成する

最初に画像を保存しているフォルダと作成する動画のファイル名を指定します。
フォルダは先ほど作成したimageフォルダです。
```python
input_dir   = 'image'           # 入力する画像が保存されたフォルダ名
output_path = input_dir + '_ff' + '.mp4'    # 作成する 動画ファイル
print(output_path)
```

指定したフォルダ内のjpgファイルとpngファイルの一覧を取得して名前順で並べ替えます。
画像ファイルが一つもないとここでエラーとなります。
```python
# フォルダ内の画像のファイルリストを取得する
files  = glob.glob(os.path.join(input_dir, '*.png'))
files += glob.glob(os.path.join(input_dir, '*.jpg'))
files.sort()
frames=len(files)
assert frames != 0, 'not found image file'    # 画像ファイルが見つからない
```

最初の画像の情報を取得します。get_info=Trueとすることで情報取得以外の処理を行わないようにします。
```python
# 最初の画像の情報を取得する
ff = FFStream(files[0], get_info=True)
w, h = ff.width, ff.height      # 画像サイズ
```

取得した画像サイズを使って動画作成用のFFStreamを作成します。
必要なのは出力ストリームだけなので入力パスはNoneとします。
ピクセルフォーマットは 画像の場合'rgb24'かアルファチャンネル付きの'rgba'とします。
フレームレートは決め打ちとなります。29.97fpsの場合は30000/1001とします。

```python
# 画像サイズや フレームレートを指定して 動画を作成する
# 入力ストリームは使わないので 入力パスをNoneにし 出力パスだけ指定する
ff_output    = FFStream(None, output_path, crf=20, pix_fmt='rgb24',
                        ow=w, oh=h, ofps=30000/1001)
```

ff_input     = [None] * 6で同時に読み込む画像枚数を指定します。6なら最大6スレッド（6プロセス）使って別々の画像を同時に読み込みます。
```python
input_count  = 0    # 入力フレーム数
output_count = 0    # 出力フレーム数
ff_input     = [None] * 6   # 画像入力用のFFmpegを格納する入れ物を複数用意する (最大6つの画像を同時に読み込む)
```

メインとなる画像読み込みと 動画出力処理です。
処理の流れとしては最初に6個の入力ストリームでそれぞれ1から6枚目の画像を読み込むように指示を出して、それから1枚目より順に回収して動画へ出力します。この時 次の画像（7枚目）を読み込むように新しい入力ストリームへ指示します。
ポイントは読み込み指示から回収の間に別の処理（別の入力ストリームの処理）が行えるのでマルチスレッドのように動作することです。


```python
bar = tqdm(total=frames, dynamic_ncols=True)
while True:
    for ct in range(len(ff_input)):
        # 入力ストリームが有効な場合は 画像を回収する
        if ff_input[ct] != None:
            # FFmpegから画像を受け取り 動画へ出力する
            frame_bytes = ff_input[ct].recv_frame()
            ff_output.send_frame(frame_bytes)
            output_count += 1
            bar.update(1)

        # 入力するフレームが残っている場合は 新しい入力ストリームへ画像を読み込む指示を出す
        if input_count < frames:
            # 画像サイズなどの情報を取得するffprobeは遅いので使わない 代わりにow等で指定する 
            ff_input[ct] = FFStream(files[input_count], None, pix_fmt='rgb24', ow=w, oh=h, no_probe=True)
            input_count += 1
        elif ff_input[ct] != None:
            # 最終フレームの読み込みが終わったので 入力ストリームを無効にする
            ff_input[ct] = None

    # 最終フレームを出力したら 処理終了
    if output_count == frames:
        break
       
bar.close()
ff_output.close()
```



# 全体
```python:test_img_ffstream.py
import os
import glob
from tqdm import tqdm
from ffstream.ffstream import FFStream

input_dir   = 'image'           # 入力する画像が保存されたフォルダ名
output_path = input_dir + '_ff' + '.mp4'    # 作成する 動画ファイル
print(output_path)

# フォルダ内の画像のファイルリストを取得する
files  = glob.glob(os.path.join(input_dir, '*.png'))
files += glob.glob(os.path.join(input_dir, '*.jpg'))
files.sort()
frames=len(files)
assert frames != 0, 'not found image file'    # 画像ファイルが見つからない
    
# 最初の画像の情報を取得する
ff = FFStream(files[0], get_info=True)
w, h = ff.width, ff.height      # 画像サイズ

# 画像サイズや フレームレートを指定して 動画を作成する
# 入力ストリームは使わないので 入力パスをNoneにし 出力パスだけ指定する
ff_output    = FFStream(None, output_path, crf=20, pix_fmt='rgb24',
                        ow=w, oh=h, ofps=30000/1001)
input_count  = 0    # 入力フレーム数
output_count = 0    # 出力フレーム数
ff_input     = [None] * 6   # 画像入力用のFFmpegを格納する入れ物を複数用意する (最大6つの画像を同時に読み込む)

bar = tqdm(total=frames, dynamic_ncols=True)
while True:
    # 複数の画像を読み込み 順番に動画へ出力する
    for ct in range(len(ff_input)):
        if ff_input[ct] != None:
            # FFmpegから画像を受け取り 動画へ出力する
            frame_bytes = ff_input[ct].recv_frame()
            ff_output.send_frame(frame_bytes)
            output_count += 1
            bar.update(1)

        if input_count < frames:
            # 画像読み込み用のFFmpegを起動する
            # 画像サイズなどの情報を取得するffprobeは遅いので使わない 代わりにow等で指定する 
            ff_input[ct] = FFStream(files[input_count], None, pix_fmt='rgb24', ow=w, oh=h, no_probe=True)
            input_count += 1
        elif ff_input[ct] != None:
            # 入れ物を無効にする
            ff_input[ct] = None
            
    if output_count == frames:
        break
       
bar.close()
ff_output.close()
```





# 結果
画像1枚につきffmpeg.exeを一つ起動するのと、プロセス間通信でデータを受け取る必要があるため これらのオーバーヘッドが大きく思ったより早くはなりませんでした。
画像サイズが小さいとオーバーヘッドが上回り逆効果で、OpenCVを使ったシングルスレッド処理の方が数倍早かったです。

- SD(640x480)以下ではOpenCVの方が断然早い
- SD(640x480)からFHD(1920x1080)の間で逆転しFFStreamが少し早い
- FHD(1920x1080)以上ではFFStreamを使ったほうが速い