# 推断文档
English | 中文
## 目录
* [概念](#概念)
    * [下采样比](#下采样比)
    * [循环记忆](#循环记忆)
* [PyTorch](#pytorch)
* [TorchHub](#torchhub)
* [TorchScript](#torchscript)
* [ONNX](#onnx)
* [TensorFlow](#tensorflow)
* [TensorFlow.js](#tensorflowjs)
* [CoreML](#coreml)
## 概念
### 下采样比
该表仅供参考。可根据视频内容进行调节。
| 分辨率         | 人像           | 全身            |
| ------------- | ------------- | -------------- |
| <= 512x512    | 1             | 1              |
| 1280x720      | 0.375         | 0.6            |
| 1920x1080     | 0.25          | 0.4            |
| 3840x2160     | 0.125         | 0.2            |
模型在内部将高分辨率输入缩小做初步的处理,然后再放大做细分处理。
建议设置 `downsample_ratio` 使缩小后的分辨率维持在 256 到 512 像素之间. 例如,`1920x1080` 的输入用 `downsample_ratio=0.25`,缩小后的分辨率 `480x270` 在 256 到 512 像素之间。
根据视频内容调整 `downsample_ratio`。若视频是上身人像,低 `downsample_ratio` 足矣。若视频是全身像,建议尝试更高的 `downsample_ratio`。但注意,过高的 `downsample_ratio` 反而会降低效果。
### 循环记忆
此模型是循环神经网络(Recurrent Neural Network)。必须按顺序处理视频每帧,并提供网络循环记忆。
**正确用法**
循环记忆输出被传递到下一帧做输入。
```python
rec = [None] * 4  # 初始值设置为 None
for frame in YOUR_VIDEO:
    fgr, pha, *rec = model(frame, *rec, downsample_ratio)
```
**错误用法**
没有使用循环记忆。此方法仅可用于处理单独的图片。
```python
for frame in YOUR_VIDEO:
    fgr, pha = model(frame, downsample_ratio)[:2]
```
更多技术细节见[论文](https://peterl1n.github.io/RobustVideoMatting/)。
## PyTorch
载入模型:
```python
import torch
from model import MattingNetwork
model = MattingNetwork(variant='mobilenetv3').eval().cuda() # 或 variant="resnet50"
model.load_state_dict(torch.load('rvm_mobilenetv3.pth'))
```
推断循环:
```python
rec = [None] * 4 # 初始值设置为 None
for src in YOUR_VIDEO:  # src 可以是 [B, C, H, W] 或 [B, T, C, H, W]
    fgr, pha, *rec = model(src, *rec, downsample_ratio=0.25)
```
* `src`: 输入帧(Source)。 
    * 可以是 `[B, C, H, W]` 或 `[B, T, C, H, W]` 的张量。 
    * 若是 `[B, T, C, H, W]`,可给模型一次 `T` 帧,做一小段一小段地处理,用于更好的并行计算。
    * RGB 通道输入,范围为 `0~1`。
* `fgr, pha`: 前景(Foreground)和透明度通道(Alpha)的预测。 
    * 根据`src`,可为 `[B, C, H, W]` 或 `[B, T, C, H, W]` 的输出。
    * `fgr` 是 RGB 三通道,`pha` 为一通道。
    * 输出范围为 `0~1`。
* `rec`: 循环记忆(Recurrent States)。 
    * `List[Tensor, Tensor, Tensor, Tensor]` 类型。 
    * 初始 `rec` 为 `List[None, None, None, None]`。
    * 有四个记忆,因为网络使用四个 `ConvGRU` 层。
    * 无论 `src` 的 Rank,所有记忆张量的 Rank 为 4。
    * 若一次给予 `T` 帧,只返回处理完最后一帧后的记忆。
完整的推断例子:
```python
from torch.utils.data import DataLoader
from torchvision.transforms import ToTensor
from inference_utils import VideoReader, VideoWriter
reader = VideoReader('input.mp4', transform=ToTensor())
writer = VideoWriter('output.mp4', frame_rate=30)
bgr = torch.tensor([.47, 1, .6]).view(3, 1, 1).cuda()  # 绿背景
rec = [None] * 4                                       # 初始记忆
with torch.no_grad():
    for src in DataLoader(reader):
        fgr, pha, *rec = model(src.cuda(), *rec, downsample_ratio=0.25)  # 将上一帧的记忆给下一帧
        writer.write(fgr * pha + bgr * (1 - pha))
```
或者使用提供的视频转换 API:
```python
from inference import convert_video
convert_video(
    model,                           # 模型,可以加载到任何设备(cpu 或 cuda)
    input_source='input.mp4',        # 视频文件,或图片序列文件夹
    input_resize=(1920, 1080),       # [可选项] 缩放视频大小
    downsample_ratio=0.25,           # [可选项] 下采样比,若 None,自动下采样至 512px
    output_type='video',             # 可选 "video"(视频)或 "png_sequence"(PNG 序列)
    output_composition='com.mp4',    # 若导出视频,提供文件路径。若导出 PNG 序列,提供文件夹路径
    output_alpha="pha.mp4",          # [可选项] 输出透明度预测
    output_foreground="fgr.mp4",     # [可选项] 输出前景预测
    output_video_mbps=4,             # 若导出视频,提供视频码率
    seq_chunk=12,                    # 设置多帧并行计算
    num_workers=1,                   # 只适用于图片序列输入,读取线程
    progress=True                    # 显示进度条
)
```
也可通过命令行调用转换 API:
```sh
python inference.py \
    --variant mobilenetv3 \
    --checkpoint "CHECKPOINT" \
    --device cuda \
    --input-source "input.mp4" \
    --downsample-ratio 0.25 \
    --output-type video \
    --output-composition "composition.mp4" \
    --output-alpha "alpha.mp4" \
    --output-foreground "foreground.mp4" \
    --output-video-mbps 4 \
    --seq-chunk 12
```
## TorchHub
载入模型:
```python
model = torch.hub.load("PeterL1n/RobustVideoMatting", "mobilenetv3") # or "resnet50"
```
使用转换 API,具体请参考之前对 `convert_video` 的文档。
```python
convert_video = torch.hub.load("PeterL1n/RobustVideoMatting", "converter")
convert_video(model, ...args...)
```
## TorchScript
载入模型:
```python
import torch
model = torch.jit.load('rvm_mobilenetv3.torchscript')
```
也可以可选的将模型固化(Freeze)。这会对模型进行优化,例如 BatchNorm Fusion 等。固化的模型更快。
```python
model = torch.jit.freeze(model)
```
然后,可以将 `model` 作为普通的 PyTorch 模型使用。但注意,若用固化模型调用转换 API,必须手动提供 `device` 和 `dtype`:
```python
convert_video(frozen_model, ...args..., device='cuda', dtype=torch.float32)
```
## ONNX
模型规格:
* 输入: [`src`, `r1i`, `r2i`, `r3i`, `r4i`, `downsample_ratio`]. 
    * `src`:输入帧,RGB 通道,形状为 `[B, C, H, W]`,范围为`0~1`。
    * `rXi`:记忆输入,初始值是是形状为 `[1, 1, 1, 1]` 的零张量。
    * `downsample_ratio` 下采样比,张量形状为 `[1]`。
    * 只有 `downsample_ratio` 必须是 `FP32`,其他输入必须和加载的模型使用一样的 `dtype`。
* 输出: [`fgr`, `pha`, `r1o`, `r2o`, `r3o`, `r4o`]
    * `fgr, pha`:前景和透明度通道输出,范围为 `0~1`。
    * `rXo`:记忆输出。
我们只展示用 ONNX Runtime CUDA Backend 在 Python 上的使用范例。
载入模型:
```python
import onnxruntime as ort
sess = ort.InferenceSession('rvm_mobilenetv3_fp16.onnx')
```
简单推断循环,但此方法不是最优化的:
```python
import numpy as np
rec = [ np.zeros([1, 1, 1, 1], dtype=np.float16) ] * 4  # 必须用模型一样的 dtype
downsample_ratio = np.array([0.25], dtype=np.float32)  # 必须是 FP32
for src in YOUR_VIDEO:  # src 张量是 [B, C, H, W] 形状,必须用模型一样的 dtype
    fgr, pha, *rec = sess.run([], {
        'src': src, 
        'r1i': rec[0], 
        'r2i': rec[1], 
        'r3i': rec[2], 
        'r4i': rec[3], 
        'downsample_ratio': downsample_ratio
    })
```
若使用 GPU,上例会将记忆输出传回到 CPU,再在下一帧时传回到 GPU。这种传输是无意义的,因为记忆值可以留在 GPU 上。下例使用 `iobinding` 来杜绝无用的传输。
```python
import onnxruntime as ort
import numpy as np
# 载入模型
sess = ort.InferenceSession('rvm_mobilenetv3_fp16.onnx')
# 创建 io binding.
io = sess.io_binding()
# 在 CUDA 上创建张量
rec = [ ort.OrtValue.ortvalue_from_numpy(np.zeros([1, 1, 1, 1], dtype=np.float16), 'cuda') ] * 4
downsample_ratio = ort.OrtValue.ortvalue_from_numpy(np.asarray([0.25], dtype=np.float32), 'cuda')
# 设置输出项
for name in ['fgr', 'pha', 'r1o', 'r2o', 'r3o', 'r4o']:
    io.bind_output(name, 'cuda')
# 推断
for src in YOUR_VIDEO:
    io.bind_cpu_input('src', src)
    io.bind_ortvalue_input('r1i', rec[0])
    io.bind_ortvalue_input('r2i', rec[1])
    io.bind_ortvalue_input('r3i', rec[2])
    io.bind_ortvalue_input('r4i', rec[3])
    io.bind_ortvalue_input('downsample_ratio', downsample_ratio)
    sess.run_with_iobinding(io)
    fgr, pha, *rec = io.get_outputs()
    # 只将 `fgr` 和 `pha` 回传到 CPU
    fgr = fgr.numpy()
    pha = pha.numpy()
```
注:若你使用其他推断框架,可能有些 ONNX ops 不被支持,需被替换。可以参考 [onnx](https://github.com/PeterL1n/RobustVideoMatting/tree/onnx) 分支的代码做自行导出。
### TensorFlow
范例:
```python
import tensorflow as tf
model = tf.keras.models.load_model('rvm_mobilenetv3_tf')
model = tf.function(model)
rec = [ tf.constant(0.) ] * 4         # 初始记忆
downsample_ratio = tf.constant(0.25)  # 下采样率,根据视频调整
for src in YOUR_VIDEO:  # src 张量是 [B, H, W, C] 的形状,而不是 [B, C, H, W]!
    out = model([src, *rec, downsample_ratio])
    fgr, pha, *rec = out['fgr'], out['pha'], out['r1o'], out['r2o'], out['r3o'], out['r4o']
```
注意,在 TensorFlow 上,所有张量都是 Channal Last 的格式。
我们提供 TensorFlow 的原始模型代码,请参考 [tensorflow](https://github.com/PeterL1n/RobustVideoMatting/tree/tensorflow) 分支。您可自行将 PyTorch 的权值转到 TensorFlow 模型上。
### TensorFlow.js
我们在 [tfjs](https://github.com/PeterL1n/RobustVideoMatting/tree/tfjs) 分支提供范例代码。代码简单易懂,解释如何正确使用模型。
### CoreML
我们只展示在 Python 下通过 `coremltools` 使用 CoreML 模型。在部署时,同样逻辑可用于 Swift。模型的循环记忆输入不需要在处理第一帧时提供。CoreML 内部会自动创建零张量作为初始记忆。
```python
import coremltools as ct
model = ct.models.model.MLModel('rvm_mobilenetv3_1920x1080_s0.25_int8.mlmodel')
r1, r2, r3, r4 = None, None, None, None
for src in YOUR_VIDEO:  # src 是 PIL.Image.
    
    if r1 is None:
        # 初始帧, 不用提供循环记忆
        inputs = {'src': src}
    else:
        # 剩余帧,提供循环记忆
        inputs = {'src': src, 'r1i': r1, 'r2i': r2, 'r3i': r3, 'r4i': r4}
    outputs = model.predict(inputs)
    fgr = outputs['fgr']  # PIL.Image
    pha = outputs['pha']  # PIL.Image
    
    r1 = outputs['r1o']  # Numpy array
    r2 = outputs['r2o']  # Numpy array
    r3 = outputs['r3o']  # Numpy array
    r4 = outputs['r4o']  # Numpy array
```
我们的 CoreML 模型只支持固定分辨率。如果你需要其他分辨率,可自行导出。导出代码见 [coreml](https://github.com/PeterL1n/RobustVideoMatting/tree/coreml) 分支。