# 推断文档
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) 分支。