Python用 高速動画入出力クラス FFStream

# はじめに
FFStreamはPython上で動画を高速に入出力する機能をまとめたクラスです。
主に動画から画像を取得して AI等で処理した結果を再び動画にする目的で使います。
最初はOpenCVやPyAV等を使っていましたが、動画の処理がボトルネックになる（AIの処理より動画の処理に時間がかかる）ケースが出てきたのでffmpeg-pythonを使う方法を試みたところ大幅な改善がみられたので、クラス化しました。

:::note info
## 特徴
- [処理が速い](https://qiita.com/amaman/items/55ce75c63b81069a1d09)
- 動画の音声をコピーできる
- 画質やファイルサイズの調整ができる
- PyTorchで扱いやすい Tensor[C,H,W]形式のRGBフレームに対応
:::



:::note info
## 主な使用例
- [人物抽出を使ったグリーンバック動画作成 (rvm_alpha)](http://amalabo.blog35.fc2.com/blog-entry-420.html)
- [超解像を使って動画の画像サイズを倍にする (swin_video)](https://qiita.com/amaman/items/07a98435b10ed8e4ca51)
- [動画のフレームレートを倍にする(rife_video)](https://qiita.com/amaman/items/743c42d365a4e3bc155f)
- [複数の静止画から動画を作成する](https://qiita.com/amaman/items/f101ed6e4acc3fd437d1)
:::

![benchi_13700k.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624096/b0bc08ab-d5d9-a072-f664-d7428e8c346e.jpeg)

https://qiita.com/amaman/items/55ce75c63b81069a1d09

# ダウンロード
- [ffstream1_1_0.zip](http://www.amarectv.com/cgi-bin/kz_dl_counter.cgi?http://www.amarectv.com/download/ffstream1_1_0.zip)
- [ffstream1_2_0.zip](http://www.amarectv.com/cgi-bin/kz_dl_counter.cgi?http://www.amarectv.com/download/ffstream1_2_0.zip)
- [ffstream1_3_0.zip](http://www.amarectv.com/cgi-bin/kz_dl_counter.cgi?http://www.amarectv.com/download/ffstream1_3_0.zip)
- [ffstream1_4_1.zip](http://www.amarectv.com/cgi-bin/kz_dl_counter.cgi?http://www.amarectv.com/download/ffstream1_4_1.zip)


# 使い方
ffstream.zipを解凍してできる'ffstream'フォルダをプロジェクトへコピー


```python:ffstream_sample.py
# ffstream.pyの使用例
# 入力動画からRGB画像を受け取り 簡単な処理を加えて 音声付きの動画を作成します
import torch
from tqdm import tqdm
from ffstream.ffstream import FFStream

input_path  = 'input.mp4'                   # 入力する 動画ファイル
output_path = 'output.mp4'                  # 作成する 動画ファイル

device = torch.device('cuda')               # ビデオメモリを使って CUDAで処理する(早い)
#device = torch.device('cpu')               # メインメモリを使って CPU で処理する(遅い)

# 入力パス, 出力パス, 画質20, ピクセルフォーマットyuv420p
# 音声がある場合はコピーする, フレーム処理用のデバイスを指定する
ff = FFStream(input_path, output_path, crf=20, pix_fmt='yuv420p',
              copy_audio_stream=True, device=device)

w = ff.width        # 入力動画の 画像サイズ
h = ff.height       # 入力動画の 画像サイズ
frames = ff.frames  # 入力動画の 総フレーム数

bar = tqdm(total=frames, dynamic_ncols=True)
while True:
    # FFmpegからRGBフレームを受け取る
    frame_rgb = ff.get_rgb()
    if (frame_rgb == None) :
        break                               # 動画の最後に達した

    # RGBの処理
    frame_rgb[2]=0                          # 青成分を0にする  [C,H,W]

    # FFmpegの出力ストリームへRGBフレームを送る
    ff.put_rgb(frame_rgb)

    bar.update(1)

bar.close()
ff.close()
```




# リファレンス

## class FFStream() v1.3.0

### コンストラクタ
| 項目 | 説明 |
|:-:   |:--  |
|書式  |ff = FFStream(’input.mp4’, ’output.mp4’...)|
|引数  |下記  |
|戻り値|なし  |
|使用例|下記  |
```python:引数
    input_path:str=None		# 入力パス、省略可
    
    output_path:str=None	# 出力パス、省略可
    						# 入力パスと出力パスの両方または一方を指定して使う
          
	mbps:int=None			# 画質指定(ビットレート) 10なら10Mbpsでエンコード
 
	crf:float=None			# 画質指定(品質) 20が標準
 							# 数値が低いほど 高画質でファイルサイズが大きくなる
        
	pix_fmt:str='yuv420p'	# ピクセルフォーマット
 							# 主に 動画なら'yuv420p', jpgなら'rgb24', pngなら'rgba'
        
	copy_audio_stream:bool=False
 							# 入力動画の音声を出力動画へコピーする場合True
        					# 音声がない場合はコピーしません
             
	ss:str=None				# 開始位置
 							# '60'				60秒
        					# f'{1000/60}'		60fpsの動画の場合に1000フレーム当たり
             				# '1:30:00'			1時間30分0秒
                 
	ow:int=None				# 出力画像サイズを指定する
 							# 入力と異なる画像サイスにする場合や
        					# 入力を使わず出力だけする場合に指定する
             
	oh:int=None				# 出力画像サイズを指定する
 
	ofps:float=None			# 出力フレームレートを指定する
 							# 入力と異なる場合や 入力を使わず出力だけする場合に指定する
        					# 29.97の場合は 30000/1001
             
	image_scale:float=1		# 出力画像サイズの倍率
 
	fps_scale:float=1		# 出力フレームレートの倍率
 
	no_probe:bool=False		# プローブを使わない
 							# 画像サイズ等の情報を取得する処理は遅い
        					# 連番動画などを大量に読み込む場合Trueにする
             				# 代わりに画像サイズをow,ohで指定する
                 
	get_info:bool=False		# 入力ファイルの情報を取得するだけで ffmpegを実行しない
 
	device:str=None			# フレーム処理をビデオメモリにするかメインメモリにするか指定する
 							# 省略した場合は メインメモリ
        
	input_options:str=None	# ffmpegの入力ストリームへ設定するオプション文字列
 							# ダブルクォーテーションは任意
        					# ハードウエアデコーダーを使う例 "-hwaccel cuda"
             
    output_options:str=None	# ffmpegの出力ストリームへ設定するオプション文字列
 							# ダブルクォーテーションは任意
        					# プロファイルを指定する例 "-profile:v baseline -level:v 4.1"
             
    mt_buffer:int=0			# 1以上の場合 転送処理を別スレッドで行う
    						# その時使うバッファ数を指定する
          					# フレームの入出力は get_rgb(), put_rgb()を使う
          					# 最後は必ず def close(self):を使う               
          					# 0の場合はメインスレッドで転送する
```

使用例
```python:動画の入出力
from ffstream.ffstream import FFStream
input_path  = 'input.mp4'                   # 入力する 動画ファイル
output_path = 'output.mp4'                  # 作成する 動画ファイル
ff = FFStream(input_path, output_path)
for i in range(ff.frames):
    frame_rgb = ff.get_rgb()
    ff.put_rgb(frame_rgb)
    print(f'{i+1} / {ff.frames}')
ff.close()
```
```python:動画の入力のみ
from ffstream.ffstream import FFStream
input_path  = 'input.mp4'                   # 入力する 動画ファイル
ff = FFStream(input_path, None)
frame_rgb = ff.get_rgb()
ff.close()
```
```python:動画の出力のみ
from ffstream.ffstream import FFStream
import torch
w:int       = 1920
h:int       = 1080
fps:float   = 30000/1001
frame_rgb   = torch.ones(3,h,w)
output_path = 'output.mp4'                  # 作成する 動画ファイル
ff = FFStream(None, output_path, ow=w, oh=h, ofps=fps)
ff.put_rgb(frame_rgb)
ff.close()
```
```python:動画や画像の情報だけ取得する
from ffstream.ffstream import FFStream
input_path  = 'input.mp4'                   # 入力する 動画ファイル
ff = FFStream(input_path, None, get_info=True)
w = ff.width        # 入力動画の 画像サイズ
h = ff.height       # 入力動画の 画像サイズ
frames = ff.frames  # 入力動画の 総フレーム数
print(f'{w}x{h}, {frames}frames')
```
```python:画像を読み込む
from ffstream.ffstream import FFStream
input_path  = 'input.jpg'                   # 入力する 画像ファイル
#input_path = 'input.png'                   # 入力する 画像ファイル
ff = FFStream(input_path, None, pix_fmt='rgb24')
frame_rgb = ff.get_rgb()
ff.close()
```
```python:アルファチャンネル付きpng画像を読み込む
from ffstream.ffstream import FFStream
import torch
input_path  = 'input.png'               # 入力する 画像ファイル
ff = FFStream(input_path, None, pix_fmt='rgba')
w, h = ff.width, ff.height              # 画像サイズ
frame_bytes = ff.recv_frame()           # RGBARGBARGBARGBA
img_src     = torch.frombuffer(frame_bytes, dtype=torch.uint8)
img_src     = img_src.cuda().reshape(h,w,4)
img_src     = img_src.permute(2, 0, 1)  # 配列を整形 [C,H,W] RRRRGGGGBBBBAAAA
frame_rgb   = img_src[0:3]              # RGBフレーム
frame_alpha = img_src[3:4]              # アルファチャンネル
ff.close()
```




### recv_frame()
ローレベル入力。
FFmpegの入力ストリームから受け取ったフレームを返す。
フレームが送られてくるまで 処理は中断する。

| 項目 | 説明 |
|:-:   |:--  |
|書式  |frame_bytes = recv_frame()|
|引数  |なし  |
|戻り値|受け取ったフレーム（1次元のByte配列）<br>動画の最後まで達した場合はNoneを返す |








### send_frame(bytes)
ローレベル出力。
FFmpegの出力ストリームへフレームを送る。

| 項目 | 説明 |
|:-:   |:--  |
|書式  |send_frame(bytes)|
|引数  |- bytes<br>送るフレーム（1次元のByte配列）|
|戻り値|なし|



### close()
起動しているFFmpegのプロセスを終了する。
| 項目 | 説明 |
|:-:   |:--  |
|書式  |close()|
|引数  |なし  |
|戻り値|なし |



### get_fps()->float:
入力ストリームのフレームレートを float型で返す。
| 項目 | 説明 |
|:-:   |:--  |
|書式  |fps = get_fps()|
|引数  |なし  |
|戻り値|フレームレート（float型） |




### get_rgb()->Tensor
入力ストリームからRGBフレームを取得する。
yuv420pまたは rgb24を受け取って RGBフレーム（Tensor[C,H,W]）へ変換する。
動画の最後に達したら Noneを返す。

| 項目 | 説明 |
|:-:   |:--  |
|書式  |frame_rgb = get_rgb()|
|引数  |なし  |
|戻り値|RGBフレーム（Tensor[C,H,W]）<br>動画の最後まで達した場合はNoneを返す |




### put_rgb(frame_rgb:Tensor)
出力ストリームへRGBフレームを送る。
RGBフレーム（Tensor[C,H,W]）をyuv420pまたは rgb24へ変換する。

| 項目 | 説明 |
|:-:   |:--  |
|書式  |put_rgb(frame_rgb)|
|引数  |- frame_rgb:Tensor<br>RGBフレーム（Tensor[C,H,W]）  |
|戻り値|なし  |




### そのほか
- yuv420pのストリーム用 入出力関数
    - get_yuv420_to_rgb()->Tensor
    - put_rgb_to_yuv420(frame_rgb:Tensor)
- rgb24のストリーム用 入出力関数
    - get_rgb24_to_rgb()->Tensor
    - put_rgb_to_rgb24(frame_rgb:Tensor)