视频抠像的目标不是简单把人像框出来,而是为每个像素生成一个精确的前景不透明度,也就是alpha蒙版。过去需要人工逐帧绘制遮罩,这种流程常被称为Rotoscoping,自动化之后也常被叫做Rotomation。发丝区域因为半透明、运动和背景混叠,往往是最难处理的部分。ModNet这类无Trimap模型把视频帧当作单张图像处理,能快速得到人像蒙版;RobustVideoMatting(RVM)则在图像特征之外加入循环记忆,让抠像结果在时间维度上更连贯。本文会从原理差异讲到实际推理代码,再给出面向发丝细节的调优方法。

一、为什么发丝级视频抠像比图像抠像更麻烦
发丝之所以难抠,是因为它不像衣服或皮肤那样有清晰边界。一根头发可能只有几个像素宽,还常常出现在焦外或运动模糊中。如果只用一个二值分割结果,边缘会呈现锯齿,合成到新背景后能看到明显的白边或黑边。视频场景还增加了一个新问题:同一个发丝区域在不同帧中可能被识别成不同的透明度,播放时就会产生闪烁。这就是为什么旋转遮罩软件虽然能处理复杂边缘,但人工成本极高。
深度学习模型的出现改变了这个流程。早期方法需要用户提供Trimap,也就是明确的前景、背景和待求解区域。ModNet证明了在人物场景中,不依赖Trimap也能直接输出质量不错的alpha蒙版。它通过语义分支确定人物位置,再通过细节分支恢复发丝边缘。这样单帧抠像的速度很快,但如果直接用于视频,帧间抖动仍然存在,因为模型没有看到上一帧的信息。
RVM针对这个问题引入了循环神经网络。它的核心思想是让模型在推理每一帧时,除了当前图像,还接收上一轮计算留下的隐藏状态。隐藏状态里记录了前景外观、边缘走向和场景上下文,因此模型能做出更稳定的判断。这个设计让RVM在移动相机、人物转身、背景复杂等情况下,也能保持较连续的蒙版。
二、ModNet与RVM的架构差异与选型
ModNet的骨干网络通常采用MobileNetV2,计算量很低。它包含三个主要部分:语义估计分支负责定位人物,细节分支关注头发、手指等边界,融合分支把两部分输出合并为最终alpha。由于没有时序模块,ModNet的每一帧完全独立,适合对实时性要求很高、对帧间稳定性要求不高的场景,例如直播中的单帧背景替换。
RVM则提供了MobileNetV3和ResNet50两种骨干。MobileNetV3版本更轻,ResNet50版本精度更高。RVM的输出不仅是alpha,还包括一个前景颜色估计fgr。这个fgr很有用:当你把人物合成到新背景时,发丝边缘会残留原背景的颜色,如果直接使用原始RGB做混合,容易出现颜色渗透。RVM输出的fgr已经对前景颜色做了校正,因此合成效果更干净。
选型可以参考下面这个表。若你的任务是一张张处理照片,或者视频很短并且对速度敏感,可以先用ModNet跑通流程。如果任务是长时间的会议、访谈、Vlog视频,需要稳定发丝边缘并抑制闪烁,RVM是更合适的选择。两者并不互斥,很多项目会先使用ModNet快速验证,再切换到RVM做最终输出。
| 模型 | 输入 | 主要输出 | 优势 | 短板 |
|---|---|---|---|---|
| ModNet | 单帧RGB | alpha | 速度快、无Trimap、部署简单 | 视频帧间可能闪烁,不输出前景色 |
| RVM | 单帧RGB加隐藏状态 | alpha、前景色 | 时序稳定、边缘更连续、可去背景色 | 计算量略高,需保持循环状态 |
需要注意,RVM的循环状态必须在视频推理期间持续传递。如果对每一帧都重新初始化状态,就退化成单帧模型,时序优势会消失。因此后面的代码会显式保留rec变量,而不是对每帧调用时都创建新列表。
三、环境准备与模型加载
环境方面主要依赖PyTorch、OpenCV和Pillow。建议使用Python 3.9及以上版本。安装命令如下:
pip install torch torchvision opencv-python pillow
这里没有固定模型版本,你可以在CUDA或CPU环境下运行。如果使用GPU,记得确认torch.cuda.is_available()返回True。两个模型都可以通过torch.hub直接加载,但生产环境建议把权重下载到本地,避免每次执行都访问外网。
ModNet的加载示例如下,它会自动下载mobilenetv2权重。RVM则可以使用PeterL1n仓库下的mobilenetv3入口。初次运行会下载权重到PyTorch缓存目录,如果网络不稳定,可以手动下载后传入本地路径。
import torch
# 选择设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 加载ModNet,适合单帧快速测试
modnet = torch.hub.load("ZHKKKe/MODNet", "mobilenetv2", pretrained=True).to(device)
modnet.eval()
# 加载RVM,适合视频抠像
rvm = torch.hub.load("PeterL1n/RobustVideoMatting", "mobilenetv3").to(device)
rvm.eval()
print("模型加载完成,设备:", device)
如果权重文件已经存在本地,比如下载了rvm_mobilenetv3.pth,可以去掉torch.hub的自动下载逻辑,直接用model.load_state_dict读取。对于团队协作,建议把权重放在项目下的weights目录,并用脚本统一校验文件大小,避免出现半截文件导致的加载失败。
四、使用ModNet完成单张人像抠图
ModNet推理的基本流程是:读取图像、保持比例缩放、归一化、送入网络、拿到alpha后恢复原始尺寸。长边缩放到512是官方推荐值,再大会增加显存占用,再小会损失发丝细节。归一化必须使用0.5的均值和标准差,因为预训练权重是在这个分布上得到的。
import cv2
import numpy as np
import torch
from PIL import Image
from torchvision import transforms
def load_modnet_image(path, ref_size=512):
im = Image.open(path).convert("RGB")
orig_w, orig_h = im.size
scale = ref_size / max(orig_w, orig_h)
if scale < 1:
new_w = int(orig_w * scale)
new_h = int(orig_h * scale)
im = im.resize((new_w, new_h), Image.BILINEAR)
tensor = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])(im).unsqueeze(0).to(device)
with torch.no_grad():
_, _, matte = modnet(tensor, True)
matte = matte[0, 0].cpu().numpy()
matte = cv2.resize(matte, (orig_w, orig_h), interpolation=cv2.INTER_AREA)
return matte
拿到alpha后,通常要做一次轻微的二值化或对比度拉伸,把绝大部分背景压到0、前景压到1,同时保留发丝区域的半透明值。下面代码展示了把alpha应用到新背景的合成逻辑。注意前景颜色需要从BGR转成RGB,因为PIL读取的是RGB,而OpenCV保存时默认是BGR。
def composite_image(fg_path, bg_path, alpha_path=None):
fg = cv2.imread(fg_path)
bg = cv2.imread(bg_path)
if fg is None or bg is None:
raise ValueError("图片读取失败,请检查路径")
fg = cv2.cvtColor(fg, cv2.COLOR_BGR2RGB)
bg = cv2.cvtColor(bg, cv2.COLOR_BGR2RGB)
fg = cv2.resize(fg, (bg.shape[1], bg.shape[0]))
if alpha_path is None:
alpha = load_modnet_image(fg_path)
else:
alpha = cv2.imread(alpha_path, cv2.IMREAD_GRAYSCALE) / 255.0
alpha = alpha[..., None].astype(np.float32)
out = fg.astype(np.float32) * alpha + bg.astype(np.float32) * (1 - alpha)
out = out.astype(np.uint8)
out_bgr = cv2.cvtColor(out, cv2.COLOR_RGB2BGR)
cv2.imwrite("composite.jpg", out_bgr)
return out_bgr
这段代码在发丝边缘使用alpha混合,而不是直接把alpha阈值化。阈值化会把半透明像素全部归为前景或背景,导致发丝看起来像被剪断。实际项目中,如果发现边缘有白边,可以检查前景图是否已经包含背景光晕,因为alpha混合本身不会凭空产生白边。
五、使用RVM做稳定的视频抠像
视频处理最重要的是不要丢失循环状态。下面代码逐帧读取视频,把BGR帧转成RGB张量,再将循环状态rec传入模型。downsample_ratio是RVM的一个重要参数,官方默认0.25,表示内部把长边缩小到原来的四分之一。这个值越小,发丝细节越清晰,但计算量会明显增加。对于1080p视频,0.375通常能取得速度和质量的平衡;如果追求发丝级效果,可以降到0.2甚至0.13,但必须有足够GPU显存。
import cv2
import numpy as np
import torch
def process_video_with_rvm(src_path, out_path, downsample_ratio=0.25):
cap = cv2.VideoCapture(src_path)
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
writer = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
rec = [None] * 4
while True:
ret, frame = cap.read()
if not ret:
break
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
src = torch.from_numpy(rgb).float().div(255.0)
src = src.permute(2, 0, 1).unsqueeze(0).to(device)
with torch.no_grad():
fgr, pha, *rec = rvm(src, *rec, downsample_ratio)
pha = pha[0, 0].cpu().numpy()
fgr = fgr[0].permute(1, 2, 0).cpu().numpy()
alpha = pha[..., None]
bg = np.ones_like(fgr, dtype=np.float32) * [0.0, 1.0, 0.0]
out = fgr.astype(np.float32) * alpha + bg.astype(np.float32) * (1 - alpha)
out = (out * 255).astype(np.uint8)
out_bgr = cv2.cvtColor(out, cv2.COLOR_RGB2BGR)
writer.write(out_bgr)
cap.release()
writer.release()
print("视频处理完成:", out_path)
这里背景用了纯绿色,方便后续做色度抠像或检查边缘。fgr已经是RVM估计出的干净前景颜色,因此直接与绿幕混合不会出现明显的原背景污染。如果你要把人物合成到动态场景,只需要把bg换成一帧一帧读取的背景视频即可。需要注意的是,写视频时编码器和原始帧率要保持一致,否则音频和画面会不同步。
对于包含B帧或可变帧率的视频,建议先用FFmpeg转成恒定帧率的中间文件,例如mp4或mov,再进行推理。这样能减少帧率波动对循环状态的影响。若遇到透明通道输出需求,可以把pha单独写为灰度视频或序列帧,后期在合成软件中使用。
六、发丝级细节调优与工程经验
发丝质量通常由三个因素决定:分辨率、alpha边缘的连续性、前景颜色是否受背景污染。分辨率很好理解,输入越大,头发像素越多。但RVM和ModNet都在内部做缩放,所以一味提高输入分辨率不一定能提升细节,反而可能因为下采样损失高频信息。对于RVM,减小downsample_ratio比直接放大输入更有效。
边缘连续性主要靠RVM的循环状态。如果你发现视频开头几帧的抠像质量不稳定,可以让模型先在前20帧上跑一遍预热,或者在正式推理前先读取第一帧多次,让隐藏状态收敛。对于ModNet,由于没有时序信息,可以在后处理中对连续帧的alpha进行高斯模糊或引导滤波,减轻边缘抖动,但会增加计算成本。
前景去溢色是发丝合成中的关键一步。人物在绿色背景或逆光下拍摄时,头发边缘会吸收环境光,导致半透明像素带有绿色或蓝色。RVM输出的fgr已经做了去溢色,但仍可能存在少量残留。可以进一步使用色相抑制,比如把绿色通道在边缘区域减小,或者使用OpenCV的颜色平衡算法。如果追求极致效果,建议把alpha和fgr同时导出,在After Effects或Nuke中使用Screen合成模式。