视频相机位姿估计(MegaSaM)
更新时间:2026-09-14
简介
视频相机位姿估计算子。抽帧后先用 MoGe-2 逐帧估计单目深度与内参,再把 (image, depth, intrinsics) 喂给 MegaSaM 的 DROID-SLAM 做跟踪与全局 bundle adjustment,输出相机内参与每帧 camera-to-world 位姿。仅支持 GPU,三个 CUDA 扩展与全部权重必须离线预置,算子内零网络零编译。
功能描述
- 两级流水线:MoGe-2 出的是归一化内参,乘回 width / height 变成像素单位;第二级把每帧等比缩放到 384×512 面积、裁到 8 的倍数,内参同步按缩放比修正,深度用
nearest-exact插值到同尺寸 - DROID 吃的是原始通道序(BGR),本实现不做转换;落 npz 时才转回
(T,H,W,3)RGB,与 mega-samcvd_opt的约定一致 - 逐帧
track→track_final→terminate(_opt_intr=opt_intrinsics, full_ba=full_ba)做全局 BA;内参取droid.video.intrinsics[0] * 8.0(SLAM 在 1/8 特征分辨率上算),位姿用lietorch.SE3(traj).inv().matrix()从 w2c 转成 c2w - DROID 全部超参(
buffer/filter_thresh/warmup/keyframe_thresh/ frontend 四项 / backend 三项 /beta)逐个开放为算子参数,以普通对象而非 dict 传入(DROID 内部直接读属性) count_k默认 64:DROID 的warmup=8意味着 3 帧根本起不了轨迹;另加前置检查,抽到的帧数少于warmup时直接给出「调大count_k/max_frames」的错误信息,而不是让 SLAM 崩在内部- 大张量不进列:
intrinsic/cam_c2w以{shape, data}(形状 + 一维 float32)存出,depths默认关闭,images 完全不进列(一段 300 帧视频的 images 与 depths 是百 MB 量级)。需要完整产物时设output_npz_dir,落成键名与 mega-samcvd_opt一致(images/depths/intrinsic/cam_c2w)的 npz,文件名为<视频名>_droid.npz(视频名取路径/URL 去参数后的 stem,取不到用video_<行号>) torch.load打补丁:mega-sam 的Droid.load_weights是裸torch.load,torch>=2.6 默认weights_only=True会失败,构建 Droid 期间临时包一层weights_only=False,构建完立即还原- 两个源自 DROID 本身的既有行为:①
full_ba=True时 DROID 返回的是关键帧轨迹video.poses[:counter],逐帧的traj_filler结果在这条分支里被算出来又丢弃,所以frame_count通常小于抽帧数(实测抽 32 帧出 31 个关键帧);② 输出的depths是 MoGe 深度按 SLAM 分辨率缩放后的结果,不是 BA 优化后的depth_est(BA 返回的优化深度未被使用) - 抽帧走
VideoFrameSampler在内存里完成,不把帧写盘;输入支持video_url(含 BOS 路径)、video_base64、video_binary - 抽不到帧或单个视频估计失败时,该行张量为空、
npz_path为空串、status记录原因,不中断整列;每个视频处理完调torch.cuda.empty_cache()
算子参数
输入
| 输入 | 含义 |
|---|---|
| videos | 视频列。按 video_src_type 取值分别是视频路径/URL(含 bos://)、base64 字符串或二进制内容 |
输出
输出为一个 struct 列,字段如下(tensor 均指 struct<shape: list<int32>, data: list<float32>>,data 是按行优先展平的 float32;缺失时为 {"shape": [], "data": []}):
| 输出 | 含义 |
|---|---|
| frame_count | int32,输出的位姿帧数,等于 min(output_max_frames, 关键帧数);失败时 0 |
| image_size | list |
| intrinsic | tensor,BA 后的像素单位内参矩阵,形状 (3,3),fx=data[0]、fy=data[4] |
| cam_c2w | tensor,每帧 camera-to-world 位姿,形状 (frame_count,4,4),每个 4×4 的最后一行是 [0,0,0,1] |
| depths | tensor,深度图 (frame_count,H,W);if_output_depths=False(默认)时为空 |
| npz_path | string,落盘的 npz 绝对路径(键 images / depths / intrinsic / cam_c2w);未设 output_npz_dir 时为空串 |
| status | string,success / error: no frames / error: <异常信息> |
参数
| 参数名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| video_src_type | str | "video_url" | 输入视频编码形式:video_url / video_base64 / video_binary,其他取值抛 ValueError |
| model_path | str | "/opt/aihc/model" | 权重根目录 |
| moge_model_name | str | "Ruicheng/moge-2-vitl/model.pt" | 相对 model_path 的 MoGe-2 权重,须是 .pt 文件;默认使用 Ruicheng/moge-2-vitl |
| megasam_repo_path | str | "/opt/aihc/thirdparty/mega-sam" | mega-sam 源码根目录,须含 base/droid_slam |
| megasam_ckpt_name | str | "megasam/megasam_final.pth" | 相对 model_path 的 DROID 权重 |
| sample_mode | str | "by_count_uniform" | 抽帧模式,透传 VideoFrameSampler:by_count_uniform / by_interval_time / by_interval_frames / by_fps / by_timestamps |
| start_time_sec | float | 0.0 | 抽帧起始秒 |
| end_time_sec | float 或 None | None | 抽帧结束秒,None 表示到视频末尾 |
| count_k | int 或 None | 64 | by_count_uniform 模式抽帧数。默认取 64 是因为帧数过少(如 3 帧)起不了 DROID 轨迹 |
| interval_sec | float 或 None | None | by_interval_time 模式间隔秒 |
| interval_frames | int 或 None | None | by_interval_frames 模式间隔帧 |
| target_fps | float 或 None | None | by_fps 模式目标帧率 |
| timestamps_sec | list[float] 或 None | None | by_timestamps 模式时间戳列表 |
| max_frames | int 或 None | 300 | 抽帧上限,显存与耗时随帧数线性增长 |
| buffer | int | 1024 | DROID 关键帧缓冲区大小,短片可下调省显存 |
| filter_thresh | float | 2.0 | MotionFilter 运动阈值,低于此值的帧不作为关键帧 |
| warmup | int | 8 | frontend 启动所需帧数;抽帧数小于该值时算子直接报错 |
| keyframe_thresh | float | 2.0 | 关键帧剔除阈值 |
| frontend_window | int | 25 | frontend 优化窗口 |
| frontend_thresh | float | 12.0 | frontend 边构建距离阈值 |
| frontend_radius | int | 2 | frontend 邻接半径 |
| frontend_nms | int | 1 | frontend 非极大抑制 |
| backend_thresh | float | 16.0 | backend 边构建距离阈值 |
| backend_radius | int | 2 | backend 邻接半径 |
| backend_nms | int | 3 | backend 非极大抑制 |
| beta | float | 0.3 | 几何 / 运动项权重 |
| opt_intrinsics | bool | True | 是否在 BA 里一并优化内参(对应 DROID terminate 的 _opt_intr);DROID 内部若判定焦距不可观测会自动关掉 |
| full_ba | bool | True | 是否做全帧 BA(对应 DROID terminate 的 full_ba) |
| output_max_frames | int | 1000 | 输出帧数上限 |
| if_output_depths | bool | False | 是否把深度图写进输出列,默认关(单帧 384×512 就是 20 万个 float32) |
| output_npz_dir | str 或 None | None | 非空时把 images / depths / intrinsic / cam_c2w 落成 npz,目录会自动创建 |
| video_format | str | "mp4" | 二进制 / base64 输入的容器格式 |
| rank | int | 0 | 多卡场景 worker 序号,只影响 MoGe 的落卡 |
注意事项
- 三个 CUDA 扩展需要提前按目标 GPU 架构编好:
droid_backends、lietorch、torch_scatter。算子只做 import 检查,缺失时抛ImportError,运行期不做编译。 - 编译产物与 python 版本、torch 版本、GPU sm 架构三重绑定:换 python 小版本、换 torch 或换卡都得重编;要同时覆盖多种架构的卡,需要编出含对应架构的 fat binary。
- 必须有 GPU:无 CUDA 时
__init__直接RuntimeError,DROID-SLAM 的 CUDA 扩展没有 CPU 实现。 moge_model_name必须指到.pt文件:MoGe 的from_pretrained传目录会被当成 HF repo id 处理而报错。megasam_ckpt_name同样指向具体的.pth文件,两个路径任一不存在即FileNotFoundError;megasam_repo_path下缺base/droid_slam也会直接报错。- 多卡靠 worker 级隔离:
Droid.load_weights内部把网络固定放到cuda:0,算子的rank只影响 MoGe 落卡,多卡场景需要 worker 级CUDA_VISIBLE_DEVICES。 - 按
concurrency=1起:测试用法是num_cpus=4, num_gpus=1, concurrency=1, batch_size=1;buffer默认 1024 个关键帧槽位占显存,短片建议下调(测试用 256)。实测test_video.mp4抽 32 帧全程 85 秒,npz 42.7M。 - 首帧位姿只是接近单位矩阵:DROID 以第一个关键帧为世界原点,但 full BA 会把所有位姿(含首帧)一起再优化、不做 gauge 固定,实测首帧 c2w 与单位矩阵偏差约 2e-3。做校验时要留容差。
- mega-sam README 里要求的 DepthAnything / RAFT 权重是它自己 mono-depth 与
cvd_opt流程用的,本算子这条路径不需要。
调用示例
Python
1from __future__ import annotations
2
3import os
4
5import daft
6from daft import col
7
8from daft.aihc.common.udf import aihc_udf
9from daft.aihc.functions.embodied.perception.video_camera_pose import VideoCameraPose
10
11if __name__ == "__main__":
12 if os.getenv("DAFT_RUNNER", "native") == "ray":
13 import ray
14 ray.init(dashboard_host="0.0.0.0", ignore_reinit_error=True)
15 daft.set_runner_ray()
16 daft.set_execution_config(actor_udf_ready_timeout=6000, min_cpu_per_task=0)
17
18ds = daft.from_pydict({"video": ["bos://your-bucket/sample.mp4"]})
19 ds = ds.with_column(
20 "pose",
21 aihc_udf(
22 VideoCameraPose,
23 construct_args={
24 "model_path": "/path/to/models",
25 "megasam_repo_path": "/path/to/thirdparty/mega-sam",
26 "count_k": 32,
27 "buffer": 256,
28 "output_npz_dir": "/tmp/camera_pose_npz",
29 },
30 num_cpus=4,
31 num_gpus=1,
32 concurrency=1,
33 batch_size=1,
34 )(col("video")),
35 )
36 ds = ds.with_column("frame_count", col("pose")["frame_count"])
37 ds = ds.with_column("status", col("pose")["status"])
38 ds.show()
评价此篇文章
