MoGe单目几何估计实战:从仿射不变点图到三维重建部署
单目几何估计这个方向过去两年里最让我眼前一亮的开源工作MoGe 绝对排得进前三。它做的事情说起来很朴素——给你一张普通的 RGB 照片它把这张图里每个像素对应的三维点云、深度、法线、甚至相机内参一次性全给你吐出来。但真正上手跑过一遍之后你会发现它背后那套仿射不变点图的设计思路才是这个系列真正值得反复琢磨的地方。我第一次在项目里用它做场景重建的时候原本准备了两天的相机标定流程直接省掉了因为 MoGe 输出的点图本身就带尺度一致性你不需要知道相机焦距也能把几何关系恢复出来。这篇文章我打算把 MoGe 系列从设计动机到代码实操、从 ViT 骨干到点图解码、从单张推理到批量部署完整地拆一遍。不管你是刚接触单目几何的新手还是已经在做三维重建、机器人感知、AR 内容生成的从业者应该都能从里面找到能直接抄作业的东西。1. 为什么单目几何估计一直是个看起来简单做起来难的问题1.1 单张图恢复三维难在哪从一张二维照片里恢复三维结构这件事在数学上本身就是欠定的。一个像素点理论上可以对应空间中一条射线上的任意深度你没有任何额外的几何约束去确定它到底在哪。传统多视图几何靠的是视差——同一个物点在两张不同视角的图里位置有偏移通过三角测量把深度解出来。但单目只有一张图视差这条路直接堵死。那人类是怎么做的我们看一张照片能大致判断出物体远近靠的是大量的先验透视关系、遮挡顺序、纹理梯度、阴影方向、物体尺寸常识。深度学习做单目几何估计本质上就是让网络从海量数据里把这些先验学出来。MoGe 的核心贡献就是找到了一种比直接回归深度图更稳定、更通用的表达方式。这里有个关键问题直接回归深度图为什么不够好因为深度是度量量它跟相机内参强绑定。同一张图用不同焦距的相机拍深度值完全不一样。你训练的时候如果让网络直接输出米为单位的深度那网络就必须同时学会这张图大概是什么相机拍的这等于给它加了一个它根本无从判断的任务。这就是为什么早期很多单目深度模型换一个数据集就崩——它们过拟合到了训练集的相机分布上。1.2 从度量深度到仿射不变的思路转变MoGe 的破局点在于它不直接预测度量深度而是预测一个仿射不变的量。什么叫仿射不变简单说就是允许结果在尺度和平移上有一个未知的全局变换。网络输出的是相对几何结构至于这个结构整体放大多少倍、平移多少交给一个单独的模块去解。打个比方你画一张地图不需要知道这张地图是 1:1000 还是 1:5000只要地图上各个点的相对位置是对的比例尺可以后面再标。MoGe 输出的点图就是这个相对位置正确的地图而那个比例尺也就是尺度因子和偏移通过一个轻量的优化步骤恢复。这个设计带来的好处是巨大的。第一网络不用再去猜相机内参训练信号更干净泛化能力大幅提升。第二仿射不变性意味着模型对输入图像的尺度变化天然鲁棒你放大缩小图片输出的几何结构保持一致。第三它让点图这个表达可以跨数据集、跨相机统一使用这对实际部署太重要了。1.3 点图Point Map到底是个什么东西点图这个概念是 MoGe 系列的核心数据结构。你可以把它理解成一张和原图同尺寸的三维坐标图原图每个像素 (u, v) 位置点图里存的是这个像素对应的三维空间坐标 (X, Y, Z)。所以一张 H×W 的图点图就是一个 H×W×3 的张量。它和深度图的区别在于深度图只给你 Z沿相机光轴的深度点图给你完整的三维坐标。有了点图你其实同时得到了深度Z 分量、表面法线对点图求空间梯度、甚至相机内参从点图的投影关系反推。这就是为什么 MoGe 一个模型能同时输出深度、法线、内参——它们本来就是点图的不同侧面。我第一次看到这个设计的时候感觉像是被人点了一下与其让网络分别学深度、法线、内参三个任务不如让它学一个统一的中间表达其他都是这个表达的派生量。这种统一表达 派生的思路在 MoGe 后续版本里被发挥得淋漓尽致。2. ViT 骨干在 MoGe 里扮演的角色以及它为什么合适2.1 为什么选 ViT 而不是 CNNMoGe 用的是 Vision TransformerViT作为骨干网络。这个选择不是跟风而是有实打实的理由。单目几何估计需要网络理解全局的几何一致性——一个物体在画面左边和右边它们的深度关系必须自洽不能各算各的。CNN 的感受野是逐层扩大的浅层只能看到局部要建立远距离像素之间的几何约束需要堆很多层效率不高。ViT 的自注意力机制天生就是全局的。第一层开始每个 patch 就能和所有其他 patch 交互。对于几何估计这种强调整体一致性的任务这个特性太关键了。实测下来ViT 骨干在物体边缘、细长结构、大面积无纹理区域这些传统难点上表现明显比同量级的 CNN 稳。不过 ViT 也有它的代价。自注意力的计算复杂度是 patch 数量的平方高分辨率输入下显存和算力开销都很大。MoGe 的处理方式是在编码器阶段用相对低的分辨率比如把图切成 14×14 的 patch然后在解码阶段通过上采样和特征融合把分辨率提回来。这个低分辨率编码 高分辨率解码的架构是当前高精度几何估计模型的主流做法。2.2 预训练权重带来的红利MoGe 系列直接站在了大规模预训练 ViT 的肩膀上。这些骨干在 ImageNet 甚至更大规模数据上预训练过已经学到了非常丰富的视觉先验。你拿它做几何估计相当于一个已经看过海量图像的网络只需要在几何任务上做微调收敛速度和最终精度都比从零训练好得多。这里有个实操细节值得说微调的时候学习率要设得比较小尤其是骨干部分。我一般把骨干的学习率设成解码头部的十分之一左右。因为骨干的预训练特征很宝贵学习率太大容易把它冲垮反而掉点。这个经验在多个几何估计项目里都验证过。2.3 位置编码与几何先验的融合ViT 本身对位置是不敏感的位置信息全靠位置编码注入。MoGe 在位置编码上做了针对几何任务的适配。普通的 ViT 用可学习的一维位置编码但几何估计需要网络理解二维的空间关系所以 MoGe 用的是二维位置编码让网络能区分上下和左右。更关键的是MoGe 在解码阶段引入了多尺度特征。单靠 ViT 最后一层的特征分辨率太低恢复出来的点图边缘会很糊。MoGe 把编码器中间几层的特征也拿出来和最后一层一起送进解码器这样既有全局语义又有局部细节。这个多尺度融合的策略是点图能做得又准又锐利的关键。3. 仿射不变点图的解码机制拆解3.1 从特征到点图解码器做了什么解码器的任务是把 ViT 输出的特征图逐步上采样并转换成点图。这个过程分几步走。第一步把 ViT 的 patch 特征重新排列成二维特征图。第二步通过一系列上采样和卷积把分辨率从 patch 级别恢复到像素级别。第三步在最后的输出层每个像素位置输出一个三维向量就是该点的 (X, Y, Z)。这里有个设计上的巧思MoGe 输出的点图是在相机坐标系下的但带一个未知的仿射变换。也就是说网络输出的 (X, Y, Z) 和真实的三维坐标之间差一个尺度因子 s 和一个平移 t。这个 s 和 t 是全局的整张图共用一套。网络不需要知道 s 和 t 具体是多少它只需要保证输出的相对结构是对的。为什么这样设计能work因为几何结构的相对关系比如两个点之间的距离比例、三个点构成的夹角在仿射变换下是不变的。网络只要把这些相对关系学对全局的 s 和 t 可以后面再解。这就把一个大难题拆成了两个小问题网络负责学相对结构优化负责解全局变换。3.2 尺度恢复那个未知的 s 和 t怎么解尺度恢复这一步是 MoGe 从相对几何走向可用几何的关键。具体怎么做MoGe 用了一个基于优化的方法。它假设场景里存在一些已知的几何约束比如地面是平的、某些物体有标准尺寸通过这些约束反解出 s 和 t。在实际使用中最常见的做法是用相机内参来约束。如果你知道相机的焦距那么点图投影回图像平面时投影位置应该和原像素位置一致。这个一致性条件可以写成一个方程解出 s 和 t。MoGe 提供了自动估计内参的功能如果你连内参都不知道它也能从点图本身反推一个合理的焦距。我实测下来如果场景里有明显的平面结构比如桌面、墙面、地面尺度恢复会非常准。如果场景全是杂乱的无规则物体尺度恢复会有一定误差但相对结构依然可靠。所以用 MoGe 做测量类任务时最好在场景里放一个已知尺寸的参照物能显著提升绝对尺度的精度。3.3 法线和内参是怎么顺带出来的前面说过法线和内参都是点图的派生量。法线的计算很直接点图是一个三维曲面在图像平面上的参数化对它在 u 和 v 方向求偏导得到两个切向量叉乘就得到法线。MoGe 在训练时会把法线作为一个辅助监督信号让点图的局部结构更平滑、更符合真实表面。内参的恢复稍微绕一点。点图里的三维点投影到图像平面应该落在原来的像素位置。这个投影关系里就藏着焦距。MoGe 通过最小化重投影误差把焦距解出来。实测中MoGe 估计的焦距在大多数场景下误差在 5% 以内对于没有标定信息的场景来说这个精度已经相当能打了。4. 把 MoGe 跑起来从环境配置到第一张点图4.1 环境准备里最容易踩的坑MoGe 的官方实现依赖 PyTorch 和一些常见的视觉库。环境配置本身不复杂但有几个坑我踩过这里提前说。第一PyTorch 版本要和 CUDA 版本匹配这个老生常谈但每次都有人栽。第二MoGe 用了一些较新的算子如果你的 PyTorch 太老某些层会报错。建议用 2.0 以上的版本。第三也是最容易忽略的显存。MoGe 的 ViT 骨干在高分辨率输入下显存占用不小。我一开始用 24G 的卡跑 1024×1024 的输入batch size 只能设 1。如果你想批量处理要么降分辨率要么用梯度累积。实测 512×512 的输入24G 卡能跑到 batch size 4 左右速度也快很多。# 建议的环境配置流程 conda create -n moge python3.10 conda activate moge pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python pillow numpy scipy # 然后按官方仓库说明安装 moge 本体4.2 单张图推理的完整流程跑通第一张点图其实就几行代码。加载模型、读图、预处理、前向、后处理。但每一步都有细节。import torch from moge.model import MoGeModel from PIL import Image import numpy as np # 加载模型会自动下载预训练权重 model MoGeModel.from_pretrained(Ruicheng/moge-vitl).to(cuda) model.eval() # 读图并转成 tensor img Image.open(test.jpg).convert(RGB) img_tensor torch.tensor(np.array(img) / 255.0, dtypetorch.float32) img_tensor img_tensor.permute(2, 0, 1).unsqueeze(0).to(cuda) # 前向推理 with torch.no_grad(): output model.infer(img_tensor) # output 里包含 points点图、depth、normal、mask、intrinsics points output[points] # H x W x 3 depth output[depth] # H x W normal output[normal] # H x W x 3 intrinsics output[intrinsics] # 3 x 3这段代码里infer方法内部会自动处理分辨率对齐、归一化等细节。但你要注意输出的点图是仿射不变的也就是带未知尺度。如果你需要度量尺度得自己调model.infer里的相关参数或者用内参做后处理恢复。4.3 点图的可视化与快速验证拿到点图之后第一件事是可视化验证。最直接的方式是把点图的三维坐标投影到几个不同视角看结构对不对。我一般用 Open3D 或者简单的 matplotlib 三维散点来快速看。import matplotlib.pyplot as plt pts points.cpu().numpy().reshape(-1, 3) # 随机采样避免点太多画不动 idx np.random.choice(len(pts), 20000, replaceFalse) pts_sample pts[idx] fig plt.figure(figsize(10, 8)) ax fig.add_subplot(111, projection3d) ax.scatter(pts_sample[:, 0], pts_sample[:, 1], pts_sample[:, 2], s0.5) ax.set_box_aspect([1, 1, 1]) plt.show()如果点云看起来结构清晰、物体轮廓分明说明推理正常。如果是一团糊先检查输入图像是不是太暗或者分辨率太低。MoGe 对输入质量有一定要求太模糊的图恢复出来的几何会很粗糙。提示可视化的时候记得把点图的坐标系搞清楚。MoGe 输出的点图默认是相机坐标系X 向右、Y 向下、Z 向前。如果你直接画看到的可能是上下颠倒的这是正常的转换一下就行。5. 从单张到批量工程化部署中的实际问题5.1 批量推理的显存与速度平衡单张跑通之后下一步就是批量处理。这里最核心的矛盾是显存和速度。MoGe 的推理时间主要花在 ViT 骨干上和输入分辨率强相关。我做过一组实测在 4090 上输入分辨率单张耗时显存占用batch size 上限512×512约 0.15s约 4G8768×768约 0.3s约 8G41024×1024约 0.6s约 14G2从这张表能看出来分辨率翻倍耗时和显存都涨得很快。实际部署时要根据你的精度需求和硬件条件做权衡。如果只是做粗略的场景理解512 就够了如果要做精细重建那得上 1024。批量推理的时候我建议用torch.no_grad()加上混合精度torch.autocast能省不少显存速度也能提一截。实测混合精度下精度损失几乎可以忽略但显存能省 30% 左右。5.2 多图一致性同一场景不同视角怎么对齐MoGe 是单目模型每张图独立推理。但实际项目里你往往有同一场景的多个视角。这时候就需要把多张点图对齐到同一个坐标系。做法是对每张图分别推理得到点图然后用特征匹配或者几何约束估计两两点图之间的刚体变换最后统一到一个全局坐标系。这里有个坑MoGe 输出的点图带仿射不变性不同图的尺度可能不一致。对齐之前必须先把尺度统一。我的做法是用重叠区域的特征点做尺度估计或者如果场景里有已知尺寸的物体直接用它标定。对齐之后你可以把多张点图融合成一个完整的场景点云。这个流程在三维重建项目里非常实用比传统的 SfM 流程简单太多而且对纹理少的区域更鲁棒。5.3 和下游任务的衔接MoGe 输出的点图可以直接喂给很多下游任务。做机器人抓取的可以从点图里提取物体的位姿和尺寸做 AR 的可以用点图做虚实遮挡做三维建模的可以把点图转成网格。我重点说一下转网格这个流程因为用得最多。点图转网格最直接的方法是用泊松重建Poisson Reconstruction。Open3D 里有现成的实现。但直接对原始点图做泊松重建效果往往不好因为点图在物体边缘会有噪声。我的做法是先对点图做双边滤波平滑掉边缘噪声再做重建出来的网格质量会好很多。import open3d as o3d # 假设 pts 是 N x 3 的点云norms 是对应法线 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(pts) pcd.normals o3d.utility.Vector3dVector(norms) # 先做统计滤波去离群点 pcd, _ pcd.remove_statistical_outlier(nb_neighbors20, std_ratio2.0) # 泊松重建 mesh, densities o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd, depth9) mesh.compute_vertex_normals() o3d.io.write_triangle_mesh(output.ply, mesh)6. 那些官方文档不会告诉你的实操经验6.1 输入图像的预处理比你想的重要MoGe 对输入图像的尺寸有要求最好是 14 的倍数因为 ViT 的 patch size 是 14。如果你直接喂一张任意尺寸的图模型内部会做 padding 或者 resize但这个过程可能引入不必要的形变。我的习惯是预处理阶段就把图 resize 到 14 的倍数比如 518×518 或者 1024×1024这样能避免内部处理的额外开销。另外图像的对比度和亮度也会影响结果。太暗的图网络提取的特征质量差点图会偏。我一般会做一个简单的直方图均衡化或者至少检查一下图像的亮度分布。这个预处理步骤花不了多少时间但对结果稳定性帮助很大。6.2 边缘和遮挡区域的点图质量MoGe 在物体边缘和遮挡边界处的点图质量是所有单目几何模型的共同难点。原因是这些区域的视觉信号本身就模糊网络很难判断边界像素到底属于前景还是背景。实测下来MoGe 在边缘处会有 1-2 个像素的拖尾点图在这些位置会有一个过渡带。处理这个问题我的经验是如果下游任务对边缘敏感可以在点图上做一个基于图像边缘的引导滤波用原图的边缘信息去修正点图的边缘。或者更简单粗暴一点在边缘区域降低点图的置信度下游任务里对这些区域做特殊处理。6.3 什么时候该用 MoGe什么时候不该用MoGe 很强但不是万能的。根据我的使用经验它最适合的场景是单张图、中等尺度、有丰富纹理的室内或室外场景。这些场景下它的点图质量非常高基本可以直接用。它不太适合的场景第一极端近距离的微距拍摄因为景深太浅网络很难恢复完整几何。第二大面积纯色或无纹理区域比如白墙、天空这些地方网络只能靠上下文猜精度会下降。第三动态场景里的运动物体MoGe 假设场景是静态的运动物体会导致几何错误。知道这些边界你就能在项目里合理选择工具而不是盲目上模型。我见过太多人拿 MoGe 去跑微距图然后抱怨效果不好其实是场景选错了。6.4 模型版本的选择MoGe 系列有多个版本参数量从 ViT-S 到 ViT-L 不等。选哪个版本取决于你的精度需求和算力预算。ViT-L 精度最高但推理慢、显存大ViT-S 快但细节恢复能力弱一些。我的建议是如果做离线处理直接上 ViT-L精度优先如果做实时应用用 ViT-S 或者 ViT-B配合降分辨率能跑到实时。中间还有个 ViT-B是个不错的平衡点大多数项目用这个就够了。7. 点图这个表达还能怎么玩7.1 点图作为三维理解的统一接口MoGe 让我最兴奋的一点是它把点图做成了一个通用的三维理解接口。以前做三维任务深度、法线、内参、位姿每个都是独立的模块接口不统一集成起来很麻烦。现在有了点图所有这些信息都从一个统一的表达里派生出来整个系统的架构可以大大简化。我在一个机器人项目里就用了这个思路感知模块只输出点图后面的抓取规划、避障、导航全都从点图里提取需要的信息。这样感知和规划之间的接口非常干净调试起来也方便因为所有几何信息都来自同一个源头不会出现深度和法线对不上这种问题。7.2 结合时序信息做动态场景MoGe 是单帧模型但点图这个表达天然适合扩展到时序。如果你有视频流可以对每帧推理点图然后在时序上做一致性约束把动态物体和静态背景分离出来。这个思路在自动驾驶和机器人领域很有前景。具体做法是对连续帧的点图做光流对齐静态区域的点图应该能通过一个刚体变换对齐动态区域则对不上。通过对不上的区域做聚类就能把运动物体分割出来。这个方法我试过在简单场景下效果不错复杂场景还需要更多工程优化。7.3 点图与神经渲染的结合点图还有一个很有想象力的方向是和神经渲染结合。传统的神经辐射场NeRF需要多视角图像和相机位姿训练慢。如果有点图作为几何先验可以大大加速 NeRF 的训练甚至做到单图重建。思路是用 MoGe 的点图初始化 NeRF 的几何让 NeRF 只需要优化纹理和光照不用从零学几何。这样训练时间能缩短好几倍。这个方向目前还在研究中但已经有一些工作显示出了不错的效果。我个人觉得点图作为几何先验会是神经渲染走向实用的一个重要推手。8. 一些收尾的实操建议如果你准备在自己的项目里用 MoGe我给几条实在的建议。第一先把官方 demo 跑通确认环境没问题再改代码。我见过太多人一上来就改源码结果环境问题和技术问题混在一起排查起来很痛苦。第二输入图像的质量决定上限花点时间做预处理比调模型参数划算得多。第三点图的尺度恢复一定要重视如果你的任务需要绝对尺度务必在场景里放参照物或者确保相机内参已知。最后分享一个我常用的小技巧MoGe 推理出来的点图如果直接可视化看着有点糊别急着怀疑模型。先检查一下你的可视化方式很多时候是点云采样太稀疏或者视角没选好。把点云密度调高换个正对场景的视角你会发现点图其实比第一眼看到的清晰得多。这个坑我踩过不止一次后来养成了先调可视化再判断模型质量的习惯。