StereoCrafter 源码级深度拆解:腾讯如何把 2D 视频“抠“成 3D

一、开场白:为什么 2D→3D 值得被认真对待

2024 年苹果把 Apple Vision Pro 塞进了开发者手里,2025 年起各种 3D 显示器、裸眼 3D 手机屏也陆续铺开。但一个尴尬的事实摆在那:内容端完全跟不上硬件端。人类过去一百年攒下的视频几乎全是 2D 单目拍摄的,指望好莱坞重新拍一遍 3D 版不现实------所以"把任意 2D 视频转换成可观看的立体 3D 视频"就成了一个既有学术价值、又有商业想象力的问题。

这活儿听起来简单:给左眼画面补一个右眼画面不就完了?但难点在于------右眼画面不是"画"出来的,是"算"出来的。它必须满足两个硬约束:

  1. 几何一致:左右眼画面的视差(disparity)必须符合真实三维场景的几何关系,否则大脑立刻罢工,出现眩晕;
  2. 时间一致:每一帧的立体关系不能跳变,否则看几秒就头晕想吐。

传统方法(比如直接 CNN 生成右视图)在这两点上都不太行。腾讯 ARC Lab 的 StereoCrafter 给出的答案很有意思:不跟几何死磕,而是"先用几何算个八九不离十,再用扩散模型把漏洞补上"------一个经典的"粗到细"(coarse-to-fine)工程哲学。

本篇文章就从源码出发,把它扒个底朝天。


二、项目速览

项目 信息
名称 StereoCrafter
团队 Tencent AI Lab / ARC Lab(赵思杰、胡文波、寸晓东、张勇、李晓宇等)
论文 arXiv:2409.07447(2024.09 提交技术报告)
项目主页 stereocrafter.github.io
代码 GitHub: TencentARC/StereoCrafter
模型权重 HuggingFace: TencentARC/StereoCrafter
首版开源 2024.12.27(推理代码 + 权重)
核心卖点 任意 2D 视频 → 立体 3D,支持 3D 眼镜 / Apple Vision Pro / 3D 显示器
分支情况 main(SVD 版);v2(2026.05 更新,换成 Wan2.1-VACE-14B 骨干)

一句话概括整个系统:输入一段单目视频,输出一段可以和原视频拼成左右立体对的"右眼视频",附带遮挡掩码和可视化深度图。

整个框架分两条腿走路:

  • Stage 1:Depth-based Video Splatting ------ 用 DepthCrafter 估计视频深度,再把左视图按视差"泼"(splat)到右视图的位置,同时算出遮挡掩码;
  • Stage 2:Stereo Video Inpainting ------ 把 splatting 出的残缺右视图 + 遮挡掩码喂给一个微调过的 Stable Video Diffusion(SVD),把空洞补全,生成高保真右眼视频。

两个阶段都是开箱即用的推理脚本,不需要任何训练。


三、源码下载:带着"附件"的克隆

这个项目不是孤零零一个仓库,它依赖两个子模块(git submodule),所以官方要求用 --recursive 克隆:

bash 复制代码
git clone --recursive https://github.com/TencentARC/StereoCrafter
cd StereoCrafter

看一下根目录的 .gitmodules,两个"附件"赫然在列:

ini 复制代码
[submodule "dependency/DepthCrafter"]
	path = dependency/DepthCrafter
	url = https://github.com/Tencent/DepthCrafter

[submodule "dependency/Forward-Warp"]
	path = dependency/Forward-Warp
	url = https://github.com/wbhu/Forward-Warp.git
  • dependency/DepthCrafter:腾讯自家的视频深度估计模型(也是 SOTA 级别);
  • dependency/Forward-Warp:wbhu(论文作者胡文波)定制的前向 warp 算子,做 splatting 用,需要编译 C++/CUDA 扩展。

踩坑点 1 :如果你忘了 --recursivedependency/ 下会是两个空目录。补救命令:git submodule update --init --recursive

克隆完之后的目录结构长这样(我按实际仓库整理):

bash 复制代码
StereoCrafter/
├── .gitmodules                      # 子模块声明
├── README.md                        # 使用说明
├── License-Code.txt                 # 代码许可证
├── requirements.txt                 # Python 依赖
├── run_inference.sh                 # 一键推理脚本
├── depth_splatting_inference.py     # Stage 1:深度估计 + splatting
├── inpainting_inference.py          # Stage 2:立体视频 inpainting
├── assets/                          # 展示图
├── source_video/                    # 示例视频(camel.mp4)
├── pipelines/
│   └── stereo_video_inpainting.py   # 核心 pipeline 类(自定义 SVD inpainting 管线)
├── dependency/                      # 子模块
│   ├── DepthCrafter/                # 视频深度估计
│   └── Forward-Warp/                # 前向 splatting 算子(需编译)
└── weights/                         # (自建)存放三个模型权重

整个项目只有 3 个核心 Python 文件 + 1 个脚本 + 1 个子模块算子,代码量不大,但每一行都值得细品------这正是它适合做源码拆解的原因。


四、环境搭建:Python 3.8 + CUDA 11.8

官方声明在 Python 3.8 + CUDA 11.8 上运行。requirements.txt 内容如下:

ini 复制代码
torch==2.0.1
torchvision==0.15.2
fire==0.5.0
accelerate==1.0.1
safetensors==0.4.3
transformers==4.42.3
decord==0.6.0
diffusers==0.29.2
xformers==0.0.20
av==12.3.0
matplotlib==3.7.5
mediapy==1.2.2
opencv-python==4.10.0.84

安装命令:

bash 复制代码
pip install -r requirements.txt

然后是唯一需要"手动编译"的部件------Forward-Warp:

bash 复制代码
cd ./dependency/Forward-Warp
chmod a+x install.sh
./install.sh

踩坑点 2(CUDA 12 用户注意) :官方 commit 历史里有一个 Update inpainting_inference.py change the order to avoid segmentation fault in Cuda12 的修复------说明作者在 CUDA 12 环境下遇到过段错误,通过调整代码执行顺序规避。如果你用 CUDA 12 + 新版本 torch,建议直接切到 v2 分支(见文末),v2 官方支持 Python 3.13 + CUDA 12.8。
踩坑点 3(xformers)xformers==0.0.20 需要和 torch 2.0.1 严格配对。装不上就检查你的 CUDA 版本和 wheel 来源,xformers 是出了名的"版本洁癖"。
踩坑点 4(decord + opencv)decord==0.6.0 对视频解码格式挑剔,如果读视频报错可以试试 pip install --no-cache-dir decord;OpenCV 和 decord 都要能读你的输入视频编码,建议先用 H.264 的 mp4。


五、模型权重下载:三巨头缺一不可

在项目根目录建 weights/,然后依次拉取三个 HuggingFace 仓库(都是 git-lfs 托管的大文件):

bash 复制代码
mkdir weights && cd ./weights
git lfs install

# 1. SVD img2vid(提供 CLIP 图像编码器 + VAE)
git clone https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt-1-1

# 2. DepthCrafter(视频深度估计)
git clone https://huggingface.co/tencent/DepthCrafter

# 3. StereoCrafter 自家权重(微调后的 UNet)
git clone https://huggingface.co/TencentARC/StereoCrafter

三个权重各司其职:

权重 提供什么 被谁使用
SVD img2vid CLIP image_encoder、VAE、scheduler、基础配置 Stage 1 的 DepthCrafter pipeline 组件 + Stage 2 的骨架
DepthCrafter 视频深度估计 UNet(DiffusersUNetSpatioTemporalConditionModelDepthCrafter) Stage 1 深度估计
StereoCrafter 微调后的 UNet(unet_diffusers 子目录) Stage 2 立体 inpainting

踩坑点 5(磁盘空间) :三个仓库加起来十几个 GB。国内直连 HuggingFace 慢的话,可以用 hf-mirror.com 镜像:git clone https://hf-mirror.com/stabilityai/stable-video-diffusion-img2vid-xt-1-1


六、项目运行:一条命令,两段剧情

官方一键脚本 run_inference.sh 长这样:

bash 复制代码
# Stage 1(默认被注释掉,因为通常先跑一次即可)
# python depth_splatting_inference.py \
#    --pre_trained_path ./weights/stable-video-diffusion-img2vid-xt-1-1 \
#    --unet_path ./weights/DepthCrafter \
#    --input_video_path ./source_video/camel.mp4 \
#    --output_video_path ./outputs/camel_splatting_results.mp4

# Stage 2
python inpainting_inference.py \
    --pre_trained_path ./weights/stable-video-diffusion-img2vid-xt-1-1 \
    --unet_path ./weights/StereoCrafter \
    --input_video_path ./outputs/camel_splatting_results.mp4 \
    --save_dir ./outputs \
    --tile_num 2

注意顺序:先跑 Stage 1 得到 splatting 结果视频,再把它喂给 Stage 2。Stage 1 输出的是 2×2 宫格视频(左上原图、右上深度可视化、左下遮挡掩码、右下 splatting 右视图),Stage 2 会解析这个宫格,取出 warped 帧和 mask,生成最终的 side-by-side 立体视频 + 红蓝 anaglyph 视频。

两个脚本都用了 Fire(main)------Google 的 Fire 库,把 main() 的参数自动变成命令行参数,所以你可以直接传参覆盖默认值,比如调 --max_disp 控制视差强度。


七、源码调试拆解(Stage 1):depth_splatting_inference.py

这个文件是整条流水线的第一步,逻辑链是:读视频 → 调 DepthCrafter 估计深度 → 深度转视差 → 前向 splatting 生成右视图 + 遮挡掩码。我们逐块拆。

7.1 读视频:read_video_frames

python 复制代码
def read_video_frames(video_path, process_length, target_fps, max_res, dataset="open"):
    vid = VideoReader(video_path, ctx=cpu(0))
    original_height, original_width = vid.get_batch([0]).shape[1:3]
    height = round(original_height / 64) * 64
    width = round(original_width / 64) * 64
    if max(height, width) > max_res:
        scale = max_res / max(original_height, original_width)
        height = round(original_height * scale / 64) * 64
        width = round(original_width * scale / 64) * 64
    ...
    fps = vid.get_avg_fps() if target_fps == -1 else target_fps
    stride = round(vid.get_avg_fps() / fps)
    frames_idx = list(range(0, len(vid), stride))
    ...
    frames = vid.get_batch(frames_idx).asnumpy().astype("float32") / 255.0
    return frames, fps, original_height, original_width

几个关键设计:

  • 64 对齐:高度宽度都向上取整到 64 的倍数------这是为了兼容 VAE 的 8× 下采样和 UNet 的时空注意力要求;
  • max_res 限幅:默认 1024,超出则等比缩放,防止深度估计阶段爆显存;
  • stride 采样:通过抽帧把帧率降下来,控制推理帧数(DepthCrafter 每窗口只处理 70 帧,overlap 25 帧)。

7.2 深度估计:DepthCrafterDemo

python 复制代码
class DepthCrafterDemo:
    def __init__(self, unet_path, pre_trained_path, cpu_offload="model"):
        unet = DiffusersUNetSpatioTemporalConditionModelDepthCrafter.from_pretrained(
            unet_path, low_cpu_mem_usage=True, torch_dtype=torch.float16)
        self.pipe = DepthCrafterPipeline.from_pretrained(
            pre_trained_path, unet=unet, torch_dtype=torch.float16, variant="fp16")
        if cpu_offload is not None:
            if cpu_offload == "sequential":
                self.pipe.enable_sequential_cpu_offload()
            elif cpu_offload == "model":
                self.pipe.enable_model_cpu_offload()
        ...
        try:
            self.pipe.enable_xformers_memory_efficient_attention()
        except Exception as e:
            print(e); print("Xformers is not enabled")
        self.pipe.enable_attention_slicing()

架构洞察 :DepthCrafter 的 pipeline 直接复用了 SVD 的 VAE 和 scheduler------这就是为什么 Stage 1 的 --pre_trained_path 指向 SVD 权重。DepthCrafter = SVD 骨架 + 自定义时空条件 UNet + 深度输出头,和 StereoCrafter 微调 UNet 是同一套"借壳上市"玩法。

infer() 里值得注意的细节:

python 复制代码
with torch.inference_mode():
    res = self.pipe(frames, height=frames.shape[1], width=frames.shape[2],
                    output_type="np", guidance_scale=1.2,
                    num_inference_steps=8, window_size=70, overlap=25).frames[0]

# 三通道深度图 → 单通道
res = res.sum(-1) / res.shape[-1]
# 上采样回原始分辨率
tensor_res = torch.tensor(res).unsqueeze(1).float().contiguous().cuda()
res = F.interpolate(tensor_res, size=(original_height, original_width),
                    mode='bilinear', align_corners=False)
res = res.cpu().numpy()[:, 0, :, :]
# 全视频归一化到 [0, 1]
res = (res - res.min()) / (res.max() - res.min())
  • 8 步去噪 + guidance_scale=1.2:Diffusion 深度估计用极少步数就够了,这是工程上的精打细算;
  • 深度归一化是"整段视频级"res.min()/res.max() 取的是全片极值,保证深度在时间轴上可比------这直接决定了后续视差转换的一致性;
  • 可选 save_depth 把深度存成 .npz 和可视化 mp4,方便调试。

7.3 前向 splatting 核心:ForwardWarpStereo

这是全项目最有技术含量的一个类,务必逐行看:

python 复制代码
class ForwardWarpStereo(nn.Module):
    def __init__(self, eps=1e-6, occlu_map=False):
        super().__init__()
        self.eps = eps
        self.occlu_map = occlu_map
        self.fw = forward_warp()

    def forward(self, im, disp):
        im = im.contiguous()
        disp = disp.contiguous()
        weights_map = disp - disp.min()
        weights_map = 1.414 ** weights_map   # 用 1.414 代替 exp,防数值上溢
        flow = -disp.squeeze(1)
        dummy_flow = torch.zeros_like(flow, requires_grad=False)
        flow = torch.stack((flow, dummy_flow), dim=-1)   # 只水平方向移动
        res_accum = self.fw(im * weights_map, flow)      # 加权像素累加
        mask = self.fw(weights_map, flow)
        mask.clamp_(min=self.eps)
        res = res_accum / mask                            # 归一化
        if not self.occlu_map:
            return res
        else:
            ones = torch.ones_like(disp, requires_grad=False)
            occlu_map = self.fw(ones, flow)
            occlu_map.clamp_(0.0, 1.0)
            occlu_map = 1.0 - occlu_map                   # 1 = 遮挡
            return res, occlu_map

拆解它的数学:

为什么用前向 warp(forward mapping)而不是后向 warp(backward warping)? 因为视差场定义的是"左视图像素 → 右视图像素"的映射方向。若用后向 warp 做 grid_sample,每个右视图像素去左视图"取值",在遮挡区域取到的可能是背景像素,语义就错了。前向 splatting 把每个左视图像素"投"到右视图,由深度决定权重,冲突时加权求和,这才是符合物理的。

权重公式

ini 复制代码
w = 1.414^disp'

其中 disp' = disp - disp.min()(相对视差,避免负指数)。论文原文用的是 w = 2^disp,代码里换成 1.414 ≈ √2,注释写得很直白:"using 1.414 instead of EXP for avoiding numerical overflow" 。含义是一样的:视差越大(离相机越近)的像素,splat 时权重越大------近处物体在遮挡关系中拥有更高优先级,这是前向 splatting 保持前景正确的关键。

归一化res_accum = Σ(w_i · im_i)mask = Σ w_ires = res_accum / mask------本质是加权平均,可微(注释里特意强调 detach will lead to unconverge!!,这是为训练留的后门,推理时无所谓)。

遮挡掩码 :把全 1 张量也 splat 一遍,被"覆盖"过的位置接近 1,没被任何像素光顾的位置为 0,occlu_map = 1 - 覆盖度,于是 1 表示空洞(需要修复),0 表示有效 。方向(flow)是 (-disp, 0)------右视图在左视图左边,所以视差取负、水平移动,垂直方向是 0(双目相机共面时只有水平视差,这是标准双目几何假设)。

7.4 组装输出:DepthSplatting

python 复制代码
disp_map = disp_map * 2.0 - 1.0    # [0,1] → [-1,1]
disp_map = disp_map * max_disp     # × 最大视差(默认 20 像素)

with torch.no_grad():
    right_video, occlusion_mask = stereo_projector(left_video, disp_map)
  • 深度 [0,1] 映射到 [-max_disp, +max_disp]max_disp 控制立体感的强度,20 像素是默认值;
  • 按 batch(默认 10 帧)分批跑,处理完 del + empty_cache() + gc.collect() 三连释放显存;
  • 输出用 cv2.VideoWriter(mp4v 编码)写成 2×2 宫格:左上原图 / 右上深度可视化 / 左下遮挡掩码 / 右下 splatting 右视图------这个宫格就是 Stage 2 的"输入素材"。

八、源码调试拆解(Stage 2):inpainting_inference.py

Stage 2 的任务:把残缺的 splatting 右视图 + 遮挡掩码喂给扩散模型,补全空洞,输出高保真右眼视频。这个文件里藏着三个有意思的设计。

8.1 解析宫格视频

python 复制代码
frames = torch.tensor(frames.asnumpy()).permute(0, 3, 1, 2).float()
height, width = frames.shape[2] // 2, frames.shape[3] // 2
frames_left = frames[:, :, :height, :width]        # 左上:原左视图
frames_mask = frames[:, :, height:, :width]        # 左下:遮挡掩码
frames_warpped = frames[:, :, height:, width:]     # 右下:splatting 右视图
frames = torch.cat([frames_warpped, frames_left, frames_mask], dim=0)

Stage 1 的宫格在这里被"拆包":取出 warped 帧、左视图、mask,然后沿着时间维拼接 成一个 3 倍帧数的张量------这不是为了炫技,而是复用 SVD 原生的"多帧条件"机制,把三种输入作为一个视频序列喂进去。注意 mask 是多通道 RGB,后面会做 mean(dim=1, keepdim=True) 压成单通道。

8.2 自回归式分块:让任意长度视频保持时间一致

python 复制代码
for i in range(0, num_frames, frames_chunk - overlap):
    ...
    if generated is not None:
        input_frames_i[:cur_overlap] = generated[-cur_overlap:]  # 关键!
    ...
    video_latents = spatial_tiled_process(input_frames_i, mask_frames_i, pipeline, tile_num, ...)
    generated = torch.stack(video_frames)
    if i != 0:
        generated = generated[cur_overlap:]
    results.append(generated)

这是全文最重要的工程细节之一 :SVD 一次只能生成 14/25 帧,而视频可能有几百帧。朴素做法是每 23 帧切一块独立生成再拼接------但相邻块接缝处会出现修复区域不一致(同一处遮挡,前块补成 A,后块补成 B)。

StereoCrafter 的解法是 自回归 + overlap

  • 块大小 frames_chunk=23,步长 frames_chunk - overlap = 20,即相邻块有 3 帧重叠;
  • 当前块生成完后,把本块生成的最后 overlap 帧直接覆盖到下一块的输入开头------模型看到"上一段我补的"长什么样,就会顺着往下补,保证接缝处时间连续。

这在论文里叫 auto-regressive modeling ,训练时对应"随机把前 n 帧换成 GT"的策略。代码里对边界情况的处理(cur_icur_overlap 的重新计算)相当细致,值得反复读两遍。

8.3 Tiled 扩散:高分辨率视频的显存逃生舱

python 复制代码
def spatial_tiled_process(cond_frames, mask_frames, process_func, tile_num,
                          spatial_n_compress=8, **kargs):
    tile_overlap = (128, 128)
    tile_size = (int((height + tile_overlap[0] * (tile_num - 1)) / tile_num),
                 int((width  + tile_overlap[1] * (tile_num - 1)) / tile_num))
    tile_stride = (tile_size[0] - tile_overlap[0], tile_size[1] - tile_overlap[1])

    for i in range(tile_num):
        for j in range(tile_num):
            cond_tile = cond_frames[:, :, i*stride0 : i*stride0+ts0, j*stride1 : j*stride1+ts1]
            mask_tile = mask_frames[..., 同款切片]
            tile = process_func(frames=cond_tile, frames_mask=mask_tile, ...,
                                output_type="latent").frames[0]
            rows.append(tile)
    ...

逻辑:把高分辨率视频沿空间切成 tile_num × tile_num 个块(每块之间留 128 像素重叠),各自在潜空间做完整去噪,然后在潜空间(latent,空间尺寸是像素的 1/8)对重叠区做线性混合:

python 复制代码
# blend_h / blend_v:重叠区按线性权重混合
weight_b = torch.arange(overlap_size).view(1, 1, 1, -1) / overlap_size
b[:, :, :, :overlap_size] = (1 - weight_b) * a[:, :, :, -overlap_size:] \
                          + weight_b * b[:, :, :, :overlap_size]
  • tile_overlap = 128 像素 → 潜空间正好 16 个格子(spatial_n_compress=8);
  • 混合在潜空间做,避免像素域混合带来的块边界生硬;
  • 2K 输入用 --tile_num 2 甚至更大,官方说 tile_num 是"宽高两个维度的块数",即 tile_num=2 是 2×2 四块。

论文里有个很有说服力的消融:512×960 可以直接全局推理;1024×1920 不开 tiled 直接 OOM,开了 tiled 就能在同样显存下跑出更精细的结果。

8.4 输出合成:Side-by-Side 与红蓝 Anaglyph

python 复制代码
frames_sbs = torch.cat([frames_left, frames_output], dim=3)   # 左右并排

# anaglyph:左视图只留红色通道,右视图只留青(绿+蓝)通道
vid_left[:, :, :, 1] = 0
vid_left[:, :, :, 2] = 0
vid_right[:, :, :, 0] = 0
vid_anaglyph = vid_left + vid_right

红蓝 3D 就是最朴素的通道分离:左眼画面只保留 R 通道,右眼画面只保留 G+B 通道,叠加后戴红蓝眼镜即可分离。SBS(side-by-side)则直接交给 3D 显示器/VR 设备去处理。


九、核心 Pipeline 拆解:pipelines/stereo_video_inpainting.py

这个文件定义了一个继承自 DiffusionPipelineStableVideoDiffusionInpaintingPipeline ,是 Stage 2 的灵魂。它把 SVD 原版 pipeline 改造成"视频 inpainting"模式,核心改动集中在 __call__ 里。

9.1 与官方 SVD pipeline 的差异

环节 官方 SVD img2vid StereoCrafter 版
条件 单张首帧图 整段 warped 视频帧(每 5 帧一批编码)
UNet 输入通道 8(噪声 4 + 条件 latent 4) 9(噪声 4 + 条件 4 + mask 1)
mask 处理 _encode_mask_frames 下采样到潜空间尺寸
首帧条件 image_embeddings(CLIP) 同样保留 CLIP 条件(取 frames[0:1]

9.2 __call__ 主流程逐段解读

python 复制代码
# 3. 编码 CLIP 条件(取视频第一帧)
image_embeddings = self._encode_image(frames[0:1], device, num_videos_per_prompt, do_cfg)
# 注意:SVD 训练时用 fps-1 做微条件
fps = fps - 1

# 4. VAE 编码条件帧(分批,每 5 帧一编,省显存)
frame_latents = self._encode_vae_frames(frames, device, ...)
mask_latents = self._encode_mask_frames(frames_mask, device, ...)

# 8. 去噪循环
latent_model_input = torch.cat([latents] * 2) if do_cfg else latents
latent_model_input = torch.cat([latent_model_input, frame_latents, mask_latents], dim=2)
noise_pred = self.unet(latent_model_input, t,
                       encoder_hidden_states=image_embeddings,
                       added_time_ids=added_time_ids)[0]
# CFG:uncond + guidance_scale * (cond - uncond)
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_cond - noise_pred_uncond)
latents = self.scheduler.step(noise_pred, t, latents).prev_sample

关键洞察 1:通道拼接式条件注入 。SVD 的 UNet 输入是 [噪声latent, 条件帧latent] 的 8 通道拼接;这里把 mask 也压成 1 通道拼进来变成 9 通道,并且论文说新通道的首层卷积权重零初始化------这样微调启动时模型行为和原版一致,训练更稳。这就是"零初始化"技巧在视频 inpainting 上的应用。

关键洞察 2:CFG 的 guidance 是逐帧的

python 复制代码
guidance_scale = torch.linspace(min_guidance_scale, max_guidance_scale, num_frames).unsqueeze(0)

推理参数里 min_guidance_scale=1.01, max_guidance_scale=1.01------guidance 恒为 1.01,几乎等于不做 CFG。为什么?因为 inpainting 任务里"条件"是整段视频和 mask,信息量极大,模型已经被条件牢牢约束住了,过强的 CFG 反而会引入伪影、破坏与左视图的几何一致。这个 1.01 的选择说明作者做了充分的调参。

关键洞察 3:added_time_ids = [fps, motion_bucket_id, noise_aug_strength] 。这是 SVD 的三大微条件:fps 控制运动节奏感、motion_bucket_id 控制运动幅度、noise_aug_strength 控制输入退化程度。推理时固定 fps=7, motion_bucket_id=127, noise_aug_strength=0.0------输入帧是无噪的干净图,所以 noise_aug 为 0。

9.3 内存管理三件套

python 复制代码
# 1) VAE 分批编码条件帧
def _encode_vae_frames(self, frames, ..., n_frames_per_time=5):
    for i in range(0, frames.shape[0], n_frames_per_time):
        frame_latent = self.vae.encode(frames[i:i+n_frames_per_time]).latent_dist.mode()
        latent_list.append(frame_latent)
    frame_latents = torch.cat(latent_list, dim=0).unsqueeze(0)

# 2) decode_latents 分批解码
for i in range(0, latents.shape[0], decode_chunk_size):
    frame = self.vae.decode(latents[i:i+decode_chunk_size], **decode_kwargs).sample

# 3) fp16 陷阱处理
needs_upcasting = self.vae.dtype == torch.float16 and self.vae.config.force_upcast
if needs_upcasting:
    self.vae.to(dtype=torch.float32)
# ... 编码完再转回 fp16

AutoencoderKLTemporalDecoder 是 SVD 的时空 VAE,它在 fp16 下 force_upcast------必须临时升到 fp32 编码,否则精度崩,用完再转回来。这是跑 SVD 系列必踩的坑,官方 pipeline 同样处理了。

9.4 一个值得吐槽的细节

python 复制代码
# 在 inpainting_inference.py 里
video_latents = video_latents.unsqueeze(0)
if video_latents == torch.float16:    # ← 这行恒为 False
    pipeline.vae.to(dtype=torch.float16)

video_latents 是个 tensor,tensor == torch.float16 比较的是值而不是 dtype------这行判断永远不成立,是个潜伏的 bug(死代码) 。它不致命,因为 VAE 本来就在 fp16(且 force_upcast 路径已经处理过),但如果你是强迫症,这就是个可以提 PR 的点。源码拆解的意义就在这:不仅要看懂它做了什么,也要看懂它没做什么


十、文件关系梳理:一张图看懂数据流

整个项目的信息流可以浓缩成下面这条链:

ini 复制代码
source_video/camel.mp4
        │  (单目左视图视频)
        ▼
┌─────────────────────────────────────────────────────────┐
│  depth_splatting_inference.py       【Stage 1】          │
│                                                         │
│  read_video_frames ──► DepthCrafterDemo.infer           │
│         │                    │ (SVD组件+Depth UNet)      │
│         │                    ▼                          │
│         │             视频深度 depth [T,H,W]∈[0,1]       │
│         │                    │                          │
│         │                    ▼                          │
│         │   ForwardWarpStereo (Forward-Warp 算子)        │
│         │    disp = (depth*2-1) * max_disp              │
│         │    right_video, occlusion_mask ──┐             │
│         ▼                                  ▼             │
│   2×2 宫格视频: [原图|深度图|掩码|splatting右视图]          │
└──────────────┬──────────────────────────────────────────┘
               │  outputs/camel_splatting_results.mp4
               ▼
┌─────────────────────────────────────────────────────────┐
│  inpainting_inference.py           【Stage 2】          │
│                                                         │
│  拆包宫格: warped帧 + 左视图 + mask                      │
│        │                                               │
│        ▼                                               │
│  自回归分块(23帧, overlap 3帧)                          │
│        │                                               │
│        ▼                                               │
│  spatial_tiled_process (tile_num×tile_num 潜空间分块)     │
│        │                                               │
│        ▼                                               │
│  StableVideoDiffusionInpaintingPipeline.__call__        │
│   UNet输入 = [噪声|warped帧latent|mask latent] (9通道)   │
│   去噪 8 步 ──► decode_latents ──► 补全后的右眼视频        │
└──────────────┬──────────────────────────────────────────┘
               ▼
      outputs/camel_inpainting_results_sbs.mp4(左右并排)
      outputs/camel_inpainting_results_anaglyph.mp4(红蓝)

依赖关系

  • depth_splatting_inference.py 依赖:dependency/DepthCrafter(pipeline、UNet、可视化工具)、Forward_Warp(编译产物)、SVD 权重、DepthCrafter 权重;
  • inpainting_inference.py 依赖:pipelines/stereo_video_inpainting.py(自定义 pipeline 类)、SVD 权重(CLIP 编码器 + VAE + scheduler)、StereoCrafter 微调权重(UNet);
  • 两个脚本共享 SVD 权重作为底座------这正是这套"借 SVD 骨架"设计的经济之处:一份底座,两处复用。

十一、原理分析:把公式摊开讲

11.1 深度 → 视差:立体几何的约定

双目系统中,深度与视差近似成反比(平行相机模型):

ini 复制代码
d = f · B / Z

其中 f 是焦距,B 是基线(两眼间距),Z 是深度,d 是视差(像素)。DepthCrafter 输出的是逆深度/视差语义的深度图(近处亮、远处暗),所以代码里:

ini 复制代码
disp = (depth * 2 - 1) * max_disp

[0,1] 深度映射到 [-max_disp, +max_disp] 的视差范围。max_disp=20 意味着最远的点视差 -20px(在左视图左边 20px 处出现),最近的 +20px------这个范围控制立体强度,调太大容易眩晕,调太小没有立体感。

11.2 前向 Splatting 的加权融合

右视图每个像素可能收到多个左视图像素的"投票"(前景遮挡背景时尤其如此)。StereoCrafter 用深度加权解决冲突:

scss 复制代码
I_R(p) = Σ_q  w(q) · I_L(q)  /  Σ_q  w(q),   q 满足 p = q + (d_q, 0)
w(q)   = 1.414^(d_q - d_min)
  • 分子分母同步 splat,得到归一化颜色(对应 res_accum / mask);
  • 权重随视差指数增长:近处物体权重高,在前景/背景冲突时胜出,保证"近处盖远处"的正确层叠顺序;
  • 1.414 ≈ √2 而非 e,是工程上的数值稳定妥协。

11.3 遮挡掩码的生成

scss 复制代码
occ(p) = 1 - FW(ones)(p)

把全 1 张量按同样的 flow splat,得到"每个像素被多少源像素覆盖"的图,1 - 覆盖度 就是遮挡掩码:没被任何像素光顾的 = 遮挡空洞 = 1。这些空洞正是扩散模型要补的区域。

11.4 扩散 inpainting 的条件注入

Stage 2 本质是条件扩散生成:在每步去噪时,UNet 输入是

c 复制代码
[ z_t (噪声latent) ‖ E_vae(V_warped) ‖ E_mask(M) ]     ← 9 通道

条件(warped 帧 + mask)在潜空间与噪声对齐拼接,模型学习的是 P(V_right | V_warped, M)。零初始化保证微调初期模型输出 ≈ 原版 SVD,然后逐步"学会"把空洞按真实右视图的分布补上。扩散模型的优势是生成的不是"平均模糊解",而是有纹理、有细节的合理猜测------这正是它吊打传统 inpainting(FuseFormer/E2FGVI)的地方。

11.5 训练配方(论文 Section 4,代码未开源训练部分)

  • 数据:自建 18 万序列 / 2500 万帧立体视频数据集,经 PySceneDetect 切镜头 → 视差对齐 → Match-Stereo-Videos 匹配 → splatting 生成三元组 (V_warped, M, V_right)PSNR > 25dB 过滤
  • 数据规模:25 × 576 × 1024,帧步长 1~6;
  • 优化:AdamW,lr=1e-5,8×A100,每卡 batch=1,26K 迭代,DeepSpeed Stage 2 + 梯度检查点 + fp16;
  • 自回归训练:随机把前 n 帧替换为 GT,n∈0,N------让模型学会"接着上一段继续补"。

十二、效果对比:赢在哪、怎么赢的

12.1 2D→3D 转换:vs Deep3D / Owl3D / Immersity AI

方法 右视图质量 与左视图几何一致性 时间一致性
Deep3D 整体尚可 (立体匹配结果混乱) 一般
Owl3D 较好 较好但有伪影(扶手断裂等) 一般
Immersity AI 较好 较好但有伪影 一般
StereoCrafter 高保真 (warped 帧直接约束) (自回归 + overlap)

Deep3D 是端到端 CNN 硬学"左图→右图",缺乏几何约束,生成的右视图在立体匹配下与左视图对不上;商业软件 Owl3D / Immersity 有约束但修复伪影明显。StereoCrafter 的"几何先算 + 扩散精修"组合拳在两者之间取得了平衡------非遮挡区域与左视图像素级一致(因为就是 warp 出来的),遮挡区域又有扩散模型生成细节

12.2 视频 Inpainting:vs FuseFormer / E2FGVI / ProPainter

方法 遮挡区修复 非遮挡区保真
FuseFormer 模糊 严重质量退化
E2FGVI 模糊 严重质量退化
ProPainter 模糊 一般
StereoCrafter 细节丰富 与 warp 结果高度一致

传统 inpainting 是"填充"逻辑,倾向于生成平滑模糊的中间值;StereoCrafter 是"生成"逻辑,扩散模型给出符合场景先验的细节。而且注意一个容易被忽略的点:传统方法会把非遮挡区域也改坏(因为它拿不到"哪些区域不用动"的先验),StereoCrafter 因为 mask 只在遮挡处引导,非遮挡区域基本保留 warp 结果,保真度甩开一个身位。

12.3 消融实验的结论

  1. 去掉 overlap(自回归) → 相邻块修复区时间不一致,出现闪烁;
  2. 高分辨率不开 tiled → 直接 OOM;开了 tiled → 同显存下出更精细结果;
  3. 深度估计器对比:DepthCrafter(视频深度)> Depth Anything V2(单帧深度),因为前者时间一致性更好------单帧深度在视频上跳变是立体视频的大忌。

十三、踩坑点大合集

把全文散落的坑汇总一下,按出现顺序排:

  1. 忘记 --recursivedependency/ 空目录,运行报 ModuleNotFoundError。补救:git submodule update --init --recursive
  2. Forward-Warp 编译失败 → 需要匹配的 CUDA 工具链;Windows 用户尤其痛苦(官方 install.sh 是 Linux 脚本),建议 WSL2 或 Docker。
  3. xformers 与 torch 版本错配 → 老老实实用 torch==2.0.1 + xformers==0.0.20 组合;装不上就先跳过,代码有 try/except 兜底(会打印 "Xformers is not enabled")。
  4. CUDA 12 段错误 → 作者自己踩过(有修复 commit),建议直接切 v2 分支。
  5. 显存不足 → 三板斧:cpu_offload(model / sequential)、减小 process_lengthmax_res、增大 tile_num
  6. max_disp 调太大 → 立体感强但边缘空洞多、易眩晕;调太小 → 立体感弱。默认 20 是平衡点。
  7. HuggingFace 下载慢 → 换 hf-mirror.com 镜像。
  8. 死代码观察if video_latents == torch.float16 恒 False,不影响运行但说明代码仓促。

十四、局限性 & v2 分支:故事还在继续

14.1 论文自曝的局限

  • 深度估计仍是天花板:高运动、雾、火等复杂场景下深度精度和一致性不足,直接影响 splatting 质量;
  • 不支持实时:目前是离线推理,距离"直播 2D→3D"还有距离;
  • 隐性成本:训练依赖自建的大规模立体数据集 + 8×A100,复现门槛高。

14.2 v2 分支(2026.05 更新)

2026 年 5 月官方发布了 v2:把底座从 SVD 换成 Wan2.1-VACE-14B (阿里通义万相的开源视频生成模型),环境升级到 Python 3.13 + CUDA 12.8 ,权重仓库改为 TencentARC/StereoCrafter2。这意味着:

  • 更强的视频先验(14B 参数 vs SVD 的 ~2.5B)→ 更高质量的修复;
  • 新生态(diffusers 更新版本)→ 更友好的 API;
  • 代码结构与 main 分支同构(仍是两阶段),迁移成本低。

如果你是新用户,直接上 v2;如果你想知道"扩散视频 inpainting 的最小实现长什么样",main 分支的 SVD 版反而更适合当教材------代码更短、依赖更少、逻辑更透明。


十五、总结:一套值得反复品味的工程范式

StereoCrafter 的价值不在于"发明了一个新算子",而在于它示范了一种当前 AIGC 时代极具普适性的问题解法

用可解释的传统几何打底(深度 → splatting → 遮挡),把不可解释的部分交给扩散模型(inpainting),再用工程手段(自回归、tiled、offload、零初始化微调)把"能不能跑起来"变成"跑得好不好"。

三个值得带走的源码级知识点:

  1. 前向 splatting 的加权归一化1.414^disp 深度加权 + 双 splat(颜色/权重)归一化 + 全一 splat 出遮挡掩码------25 行代码实现一个可微的立体视角合成器;
  2. 视频扩散 inpainting 的通道拼接[z_t ‖ E(video) ‖ E(mask)] 9 通道 + 零初始化新通道,是"在已有视频扩散模型上做新任务"的标准微调姿势;
  3. 自回归 + overlap 的时间一致性:不重新训练模型,仅靠"把上一段生成结果拼进下一段输入"就解决了长视频一致性------性价比极高。

源码拆解到最后你会发现,真正优秀的开源项目,往往不是代码写得有多炫,而是每一个工程决策都踩在物理/数学/经验的正确位置上。StereoCrafter 做到了,这也是它值得一读再读的原因。


参考资源:论文 arXiv:2409.07447;项目主页 stereocrafter.github.io;GitHub TencentARC/StereoCrafter(main & v2 分支);HuggingFace TencentARC/StereoCrafter & StereoCrafter2。

相关推荐
fthux5 小时前
不必下载整个仓库:GitZip Pro 让 GitHub 文件与文件夹批量下载更简单
前端·chrome·ai·edge·开源·github·firefox
人工智能研究所9 小时前
GitHub 10k+ Star:用自然语言描述系统,AI 帮你生成可交互架构图
人工智能·github·交互·自然语言·系统架构图·archify
逛逛GitHub9 小时前
一个 Skill.md 拿下 1.8 万星,这个 GitHub 项目让 AI 先说重点。
github
云空9 小时前
《GitHub 从入门到进阶完整教程》
github
Timeless15910 小时前
Git与TortoiseGit使用教程:第二章 Git常用命令及TortoiseGit使用教程
github
用户693717500138410 小时前
深夜炸场!DeepSeek 没发新模型,却重构了整个 Agent 生态
前端·后端·github
fthux10 小时前
装闭 RenoPit 源码解析(08):多模态AI调用、重试与文本降级
人工智能·ai·开源·github·open source·renopit
笨鸟先飞,勤能补拙11 小时前
AI Agent应用领域深度解析:从概念到落地的全维度审视
大数据·人工智能·python·物联网·安全·网络安全·github
JenKinJia12 小时前
Pycharm中增加远程服务器解释器无法连接的问题
服务器·pycharm·github