用 Depth Anything V2 Small 和 WebGPU 在浏览器中实现电影级景深

Dev.to AI 2026-08-05T11:50:39.290425

引言

我们最近给 Timeline Studio 加了一个「电影级景深」(Cinematic Depth)功能。Timeline Studio 是一个开源浏览器视频编辑器。这个功能通过 WebGPU 运行 Depth Anything V2 Small 模型,在本地分析图像和视频帧,再把得到的深度数据转换成可调节的景深效果。整个过程中,用户的源媒体文件完全不需要离开设备。

GitHub: MartinDelophy/ai-video-editor
在线演示:Timeline Studio

这篇文章重点讲的是,如何把一个深度估计模型变成真正可用的剪辑功能——而不是只输出一张灰度深度图的演示。

背景虚化不等于景深

典型的背景虚化功能依赖人像分割(person segmentation):

人像像素 -> 保持清晰

剩下的一切 → 应用一个统一的模糊半径。这种方案在视频通话里够用,但它无法表达场景的空间结构。一帧画面里,可能是近在眼前的树叶、站在中间的人、人背后靠墙的家具,以及远处的高楼。人像分割只能回答“是不是人”,不能告诉我们每个区域离镜头有多远。Depth Anything V2 Small 则会对整帧画面估计出连续的相对深度。有了它,编辑器就可以做到:给前景和背景施以不同的模糊;让一定深度范围内的区域保持清晰;在场景中自由移动焦点;为动态跟焦效果打好基础。

为什么选 Small 模型?浏览器运行时和 GPU 服务器不一样,有很多现实约束:初次下载的体积、GPU 与系统内存、WebGPU 的可用性、初始化延迟、处理大量视频帧的算力成本、移动端硬件条件,还有缓存和模型版本管理。我们最终选了 Depth Anything V2 Small 的 Q4F16(4 比特量化)配置,算是在效果和性能之间比较务实的平衡。

项目原本就在用 @huggingface/transformers,所以初始化的思路很直接:

import { pipeline } from "@huggingface/transformers";

let depthEstimator;

export async function getDepthEstimator(modelId, onProgress) {
  if (depthEstimator) return depthEstimator;

  depthEstimator = await pipeline("depth-estimation", modelId, {
    device: "webgpu",
    dtype: "q4f16",
    progress_callback: onProgress,
  });

  return depthEstimator;
}

上线时真正要注意的是复用:我们会保持初始化好的 worker 和 WebGPU 会话一直存活,而不是每分析一次就重建整个 pipeline。

把它做成编辑器功能,而不是模型演示

Cinematic Depth 出现在编辑器 Effects 工作台的第五张卡片上。这张卡片:

用户可以调节的参数包括:对焦距离、对焦范围、镜头模糊强度、散景高光,以及快速 / 标准 / 精细三档分析质量;同时支持启用、取消和重置操作。深度分析和视觉风格处理是两个彼此独立的阶段。

移动焦点或模糊滑块会重新合成现有的深度数据——不会重新运行模型。将深度转化为模糊:对于每个像素,我们计算其深度到所选焦点平面的距离:

export function calculateBlurAmount ({
  depth,
  focusDistance,
  focusRange,
  lensBlur,
}) {
  const distance = Math.abs(depth - focusDistance);
  return Math.max(0, distance - focusRange) * lensBlur;
}

焦点范围内的像素保持清晰;随着深度偏离该范围,模糊程度逐渐增加。Canvas 没有提供将不同模糊半径分别赋给每个像素的单一操作。一种实用的实现方式是生成源图像的多个模糊版本,并用深度蒙版将它们合成:原图

├── 轻微模糊
├── 中等模糊
└── 强模糊

这些蒙版还需要平滑和羽化处理,以减少深度不连续处产生的光晕。这虽然不是完整的物理镜头模拟,但其空间过渡效果比二值人物/背景蒙版可信得多。

单张深度图并不足以用于视频:静态图只需要一次推理,而视频会随时间变化。如果整个片段都复用第一帧的深度,运动很快会导致深度图和源帧脱节。然而,对每一原始帧都运行推理,对许多浏览器设备来说成本太高。我们采用与质量相关的时序采样:

解码视频

沿所选片段范围采样帧

使用 WebGPU 运行深度估计

存储带时间戳的深度帧

在播放和导出时复用这些深度帧。生成的数据在概念上大致如下:

const depthFrames = [
  { time: 0.0, depth: depth0 },
  { time: 0.5, depth: depth1 },
  { time: 1.0, depth: depth2 },
];

渲染时,编辑器根据当前片段相对于时间轴的时间挑选对应的深度帧。相邻采样点之间做插值,可以让过渡更平滑。

按片段范围缓存深度

深度推理的成本很高,而重新合成已经算好的深度结果则便宜得多。因此下面这些改动不应该让分析结果失效:对焦距离、焦点范围、模糊强度、散景设置、临时禁用效果。只有当素材、分析范围、质量或模型版本发生变化时,缓存才会失效。

function createDepthCacheKey ({ assetId, clipStart, clipEnd, quality, modelRevision }) {
  return [assetId, clipStart, clipEnd, quality, modelRevision].join(":");
}

缓存绑定在具体的 Visuals 或 Overlay 片段上。所有预览和导出路径都带有明确的片段 ID,这样 Overlay 效果就不会意外影响主轨道。

预览和导出必须一致

AI 剪辑功能常常在编辑器里看起来没问题,导出时却变了样。为避免这种情况,预览和导出共享:

回退到 Hugging Face
这里有一个重要的缓存问题:同一个模型对应着不同的提供方 URL。如果 URL 成为缓存身份标识,那么切换提供方就会将完全相同的文件下载两遍。我们采用与提供方无关的缓存身份。两个镜像上的文件都经过校验和验证,生产环境 URL 固定到不可变的版本号,而不是易变的 main 分支。这样可以避免:切换提供方后重复下载;仓库更新后突然不兼容;新旧权重混用;部署无法复现。

进度、取消与有用的错误信息
视频分析不能只靠一个转圈动画。界面需要区分:模型准备;WebGPU 会话初始化;视频帧分析;已完成的帧数和百分比;结果收尾。取消也不是假装取消。它通过 AbortController 和 worker 消息真正停止后续的解码与推断工作,而不是仅仅隐藏一个对话框。诸如 Failed to fetch 之类的底层消息,会被转换成可操作、本地化的错误提示,覆盖这些情况:WebGPU 不可用;网络失败;模型下载失败;用户取消。

一段 10 秒的片段需要多久?
没有诚实且不依赖设备的数字。处理时间取决于:GPU 硬件;浏览器的 WebGPU 实现;视频分辨率;时间采样密度;分析质量;模型是否已缓存;WebGPU 会话是否已初始化。首次运行包含下载、初始化和分析。后续运行大多只需要访问缓存和分析。真正重要的产品优化是:模型只下载一次;复用已初始化的会话;按质量要求采样视频;预览和导出共享深度帧;只改风格时绝不重新运行推断;提供真实进度和取消。

随时间变化的深度还能做什么?
电影感景深只是这批数据的第一种用途。同一组带时间戳的深度帧还能支持:带前后景视差的 2.5D 照片动画;文字和贴纸能准确放在主体前面或后面;由近到远的空间过渡;深度感知的智能取景;关键帧控制的焦点转移。

可复用的时间深度表示不再是临时特效,而是一种可反复使用的编辑原语。它让深度信息真正嵌入了工作流。

结语

在浏览器里跑一次模型,只能算个短暂的技术演示。要把它变成真正的编辑功能,还需要模型下发、缓存、时间映射、任务取消、响应式界面、状态持久化和导出一致性。Timeline Studio 将 Depth Anything V2 Small 整合进来,用户只需分析一次,就能反复调整、保存、恢复和导出结果。

GitHub:https://github.com/MartinDelophy/ai-video-editor
在线演示:https://video-editor.ai-creator.top/

如果这个项目对你有帮助,欢迎在 GitHub 上点个 Star。也期待你反馈问题或实现建议。

此功能仅用于对用户有权使用的媒体进行合法编辑。不得用于制作违法、侵权、虚假、误导或冒用身份的内容,也不得将 AI 生成或编辑过的输出冒充真实影像。用户需对不当使用负责。

查看原文