1. 先搞清楚 DUSt3R 到底在解决什么问题第一次看到 DUSt3R 这个工作的时候我的反应是有点懵的——三维重建这个方向已经被传统摄影测量和深度学习两条路线翻来覆去耕了十几年怎么还有人能从“输入两张图直接吐点云”这个角度切进去。后来把论文和代码都过了一遍又在自己手头几组数据上跑了几轮才慢慢品出味道它真正动手拆掉的是整套重建流程里最顽固的那块地基——对相机内外参的依赖。传统做重建不管走哪条路第一步永远是标定或者位姿估计。用 OpenCV 自己搭一套要走特征提取、匹配、RANSAC 求基础矩阵、恢复本质矩阵、分解位姿、三角化、光束法平差这一整串中间任何一步崩了后面全废用 COLMAP 这类成熟工具虽然省事但本质还是 SfM 那一套纹理弱、重叠小、视角跨度大的时候照样翻车。DUSt3R 的思路是把一对图像直接映射成一对点图pointmap让网络自己去隐式地学“这两张图在同一个三维空间里应该长什么样”位姿和深度不再需要被显式求解出来才能用。这个转变听起来像是换了个输出头实际上是问题建模方式的整体重写。以前我们把“重建”当成一个几何优化问题先求参数再代数计算DUSt3R 把它当成一个回归问题参数被塞进网络权重里前向一次就出来了。对于做工程的人来说这件事的意义在于你不再需要一套鲁棒的标定流程作为前置管道只要图像之间有足够重叠网络就敢给你一个可用的三维结构。这对那些相机参数拿不到、拍摄现场条件没法控制的场景价值相当大。这篇我打算按自己实际踩坑的顺序来写先讲整体设计思路和它为什么这么选再拆核心细节和实操要点然后给一套可以照着跑起来的流程最后把我在调试过程中撞到的那些问题整理成速查表。适合已经懂一点三维视觉基础、想上手试这个模型的人也适合只关心它能干什么、值不值得引入到自己项目里的读者。2. 整体设计与思路拆解为什么敢把位姿扔掉2.1 从“先求参数再重建”到“直接回归三维点”要理解 DUSt3R 的设计得先回到传统管道的痛点上。SfM 那一套的隐含假设是图像之间能找到足够多、足够准的对应点且相机模型是已知的内参大致可估。这两个假设在实际场景里非常脆弱——大面积的天空、白墙、玻璃幕墙特征点检测器直接失效鱼眼、广角镜头畸变严重时标准针孔模型拟合出来的内参本身就是错的。DUSt3R 的做法是把“点图”作为中间表达。所谓点图就是对图像上每个像素预测它在某个参考坐标系下的三维坐标输出形状是 H×W×3。两张图各出一张点图如果这两张点图落在同一个坐标系里那么同一个三维点在两张图上对应的像素其点图坐标应该一致——这个一致性既是训练时的监督信号也是推理时恢复相对位姿的依据。我第一次看到这个设计时的疑问是坐标系怎么统一论文里给的做法是把第一张图的相机坐标系当作参考系。也就是说网络在看图 A 和图 B 的时候输出的两张点图都表达在 A 的相机坐标系下。这样一来从 B 的点图反推 B 相机相对于 A 的位姿就变成了一个纯几何的刚体拟合问题类似 Umeyama 或 Kabsch 算法可以用 SVD 闭式求解不需要迭代优化。这里有个容易忽略的点网络必须知道“谁是第一张”。DUSt3R 用了一个非对称的编码方式让两个分支共享权重但通过交叉注意力互相看输出端天然带有主次关系。如果把这个顺序搞反了点图对应的参考系也反了后面拼全局点云时会出现整体镜像或者旋转错位。2.2 对称化设计与成对输入的取舍另一个我觉得值得说的设计是成对处理。为什么不直接一次吃进 N 张图输出全局点云原因很实在显存和训练难度。全局多视图的注意力复杂度是平方级以上而且需要大量多视图一致的数据集才能训稳。DUSt3R 选择先把两两关系做扎实再在推理阶段用图优化把成对结果拼成全局。这个取舍在工程上很聪明。成对模型的好处是输入域固定、训练目标清晰、泛化压力小坏处是推理时要构造一个“视图图”决定哪些图之间要跑一遍。这就引出了后面要讲的全局对齐环节——如果视图图选得不好误差会沿着图的边一路累积。我实测下来视图图的构造策略对最终精度影响比想象中大。最朴素的做法是全连接每两张都跑一次N 张图就是 N(N-1)/2 次前向N20 时是 190 次显存和时间都吃不消。实践里更常用的是先算图像间的相似度可以用简单的全局描述子甚至降采样后的像素差选 top-k 邻居建边再补一条最小生成树保证连通。这样既控制了计算量又避免孤立子图。2.3 与既有路线的横向对比我把几条常见路线放在一起对照方便判断什么时候该用它路线前置依赖弱纹理表现大视角跨度输出完备性上手成本传统 SfM MVS需特征匹配成功差差稠密网格高管道长单目深度估计 位姿网络需大规模训练数据中中单视图深度中神经辐射场类方法需精确位姿依赖位姿质量差高质量渲染高需逐场景优化DUSt3R仅需图像重叠较好较好成对点图中推理即用从这张表能看出来DUSt3R 的核心优势不在“精度绝对最高”而在于把前置依赖压到了最低。它不需要标定板、不需要已知内参、不需要特征匹配成功甚至不需要图像序列有序。代价是点图本身是稠密但带噪的要拿到干净的网格还得后处理。2.4 适用边界在哪这里必须泼一盆冷水。DUSt3R 不是万能药它的能力边界挺清晰图像重叠不足时直接失效。两视图重叠低于三成网络没有足够线索建立对应输出的点图会各自为政拼出来的结构是断裂的。纯旋转或者极小基线。这种情形下视差几乎为零深度不可观测网络给出的点图会退化成近似平面。这不是模型的问题是几何本身无解。动态场景。训练数据以静态场景为主画面里有明显运动物体时点图会把运动物体“钉”在错误位置且成对一致性会被破坏。超大场景的尺度漂移。成对估计的相对尺度在拼接时会累积误差几百米级别的室外场景全局对齐后仍可能看到明显形变。搞清楚这些边界比盲目调参有用得多。我见过有人拿两组几乎不重叠的图去跑然后抱怨模型不行——这属于用错了工具不是工具坏了。3. 核心细节解析点图、编码器与全局对齐3.1 点图到底怎么读点图这个词第一次接触容易误解。它不是深度图也不是视差图而是每个像素对应的三维坐标。假设输入图像分辨率是 512×384那输出就是一个 512×384×3 的张量第三个维度是 (X, Y, Z)表达在参考相机坐标系下。这个表达的好处是信息无损深度可以从中直接读出Z 分量像素到三维的映射也直接给出不需要额外的内参矩阵做反投影。坏处是它比深度图多了两个通道训练时对 XYZ 三个方向的监督权重需要平衡——如果只重点监督 ZXY 会漂如果三个方向等权远距离点的 XY 误差会被放大。实际使用时还有一个细节DUSt3R 输出的是每个像素一个三维点但这些点并不保证严格落在同一条射线上也就是说它不完全遵守针孔模型的约束。这是特性不是 bug——正因为放松了针孔约束它才能处理畸变较大的图像。但当你需要反推内参时就得用这些点去做拟合拟合残差能反映图像畸变的严重程度。3.2 编码器的双分支与交叉注意力网络结构上DUSt3R 用了两个共享权重的 ViT 分支分别处理两张图中间通过交叉注意力让两个分支交换信息。这个设计的关键在于信息交换发生在多个层级而不是只在最后融合一次。早期层的交叉注意力帮助建立低层几何对应深层则负责语义级别的区域匹配。我一开始以为共享权重会限制表达能力实测发现反而是必要的如果两个分支权重不共享网络很容易学出“一张图当查询、一张图当键”的不对称捷径泛化会变差。共享权重强迫它用同一套特征去理解两张图对应关系才稳。解码端则是把两个分支的特征重新组合输出两张点图加一个置信度图。置信度这个设计很实用——它标出了哪些像素的预测可信。后面做全局对齐时可以把低置信度的点直接剔除避免它们污染优化。实操提醒置信度阈值不要设太死。我试过卡 0.8结果纹理弱的区域被剔得只剩零星点反而让位姿拟合不稳。一般 0.3 到 0.5 之间比较稳具体要看场景纹理密度。3.3 从点图恢复相对位姿的数学过程这一步是整个流程里最“干净”的部分因为它是闭式解。给定两张点图 P1 和 P2都表达在相机 1 的坐标系下以及它们之间的像素对应关系目标是求一个旋转 R 和平移 t使得 R·P2 t ≈ P1。标准的做法是先算两团点云的质心去中心化然后构造协方差矩阵 H Σ (p2_i - μ2)(p1_i - μ1)^T对 H 做 SVDH UΣV^T则 R V·diag(1,1,det(VU^T))·U^Tt μ1 - R·μ2。那个 det 修正项是为了排除反射解很容易漏。这里有个隐含前提像素对应关系是已知的。在成对推理时这个对应是平凡的——两张图尺寸相同的话同一位置的像素就是对应点。但如果两张图分辨率不同或者经过了裁剪缩放就必须先做重采样对齐否则点图之间的对应会错位拟合出来的位姿完全是乱的。我在第一次跑的时候就栽在这上面两组图分辨率不一致输出点云像被撕开一样排查了半天才发现是预处理没对齐。3.4 全局对齐把成对结果拼成完整场景成对推理只给你两两之间的相对位姿要得到全局一致的结构需要一步全局优化。DUSt3R 论文里给的做法是把所有成对点图放到一个全局坐标系下构建一个优化问题变量是每张图的位姿目标是最小化重投影误差或者点图一致性误差。具体实现上可以用类似姿态图优化的框架节点是相机位姿边是成对估计出的相对位姿边上的权重用置信度或者拟合残差的倒数来定。求解器用现成的g2o、Ceres 那类或者直接自己写一个高斯牛顿迭代。这里最容易出问题的是尺度。成对估计出的平移量是相对的不同对之间的尺度可能不一致。解决办法是在优化时把尺度也作为变量放进去或者用一条边做基准把整体尺度固定住。我一般选重叠度最高的那一对作为尺度基准因为它的相对位姿最可信。经验之谈全局优化不要一次把几百张图全塞进去。先做增量式的从一对可信的种子开始每次加入一张与已有点云重叠最多的图优化完再加下一张。这样即使中间某张图有问题也不会把已经稳定的结构带崩。4. 实操流程从环境搭到出点云4.1 环境准备与依赖安装先说环境。这类模型对版本比较敏感尤其是 PyTorch 和 CUDA 的匹配别图省事直接装最新版。我踩过的坑是torch 2.x 某些版本和编译好的 flash-attention 不兼容导致注意力层跑不起来或者结果异常。我的建议是用 conda 建一个干净环境锁定版本conda create -n dust3r python3.10 -y conda activate dust3r pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txtrequirements.txt里通常包含 transformers、einops、roma、scipy、matplotlib 这些。如果项目里用到了自定义的 CUDA 算子比如 RoPE 的加速实现需要确认本机 nvcc 版本和 torch 的 CUDA 版本一致否则编译会失败。检查方法是python -c import torch; print(torch.version.cuda, torch.cuda.is_available()) nvcc --version两个 CUDA 版本号要能对得上比如都是 11.8。对不上就换 torch 版本别硬刚。4.2 权重下载与模型加载权重一般从官方发布的链接拿下载后核对文件大小和哈希。我遇到过一次下载中断导致文件不完整加载时报了 key 不匹配的错误排查了半天。建议下载完先做一次简单的加载测试import torch from dust3r.model import AsymmetricCroCo3DStereo model AsymmetricCroCo3DStereo.from_pretrained(path/to/weights.pth) model model.to(cuda).eval() print(loaded ok)能正常打印就说明权重没坏。这一步别跳过等跑完整流程才发现权重问题浪费的时间更多。推理时的显存占用和输入分辨率强相关。默认 512 长边的话两张图成对推理大约要 3-4 GB 显存如果提到 1024会跳到 10 GB 以上。做批量处理前先用单对图测一下峰值显存import torch torch.cuda.reset_peak_memory_stats() # 跑一次推理 print(torch.cuda.max_memory_allocated() / 1024**3, GB)有了这个数字再决定 batch size 和分辨率比拍脑袋靠谱。4.3 图像预处理的关键参数预处理这一步看起来琐碎实际影响很大。主要涉及三个量长边缩放尺寸、中心裁剪策略、像素值归一化。长边缩放到 512 是官方默认能兼顾速度和精度。如果下游任务需要更精细的结构可以放到 768 或 1024但要相应减 batch。裁剪方面尽量保持图像原始长宽比用 padding 补齐比直接裁剪安全——裁剪会把边缘的结构信息切掉而这些信息在点图拼接时是有用的。归一化用 ImageNet 的均值和方差即可模型是在这个分布上训的mean [0.5, 0.5, 0.5] std [0.5, 0.5, 0.5]注意如果输入图像本身曝光差异很大比如同一场景一张欠曝一张过曝建议先做一次简单的直方图匹配或者 CLAHE让两张图的亮度分布接近。网络对亮度差异有一定鲁棒性但差异过大时对应关系会变差点图一致性下降。4.4 成对推理与点图落盘跑成对推理的核心调用大致是这样from dust3r.inference import inference from dust3r.utils.image import load_images images load_images([a.jpg, b.jpg], size512) output inference([tuple(images)], model, devicecuda, batch_size1)输出里包含两张点图、置信度图和每张图的特征。点图的形状是 (H, W, 3)置信度是 (H, W)。落盘的时候建议把点图和置信度一起存用 npz 格式后面做全局对齐时能直接用。import numpy as np np.savez(pair_ab.npz, pts1output[pred1][pts3d].cpu().numpy(), pts2output[pred2][pts3d_in_other_view].cpu().numpy(), conf1output[pred1][conf].cpu().numpy(), conf2output[pred2][conf].cpu().numpy())注意pts3d_in_other_view这个字段名——它表示第二张图的点图已经被表达在第一张图的坐标系下了这正是全局对齐需要的量。我第一次存的时候拿错了字段存成了第二张图自身坐标系下的点图后面拼全局时所有位姿都错白折腾了一天。4.5 全局对齐的实现要点拿到所有成对结果后构造一个图节点是图像编号边是成对估计。然后做增量优化。我的实现思路是先用一个简单的并查集把图分成若干连通分量每个分量内部选一条置信度最高的边作为种子。种子边直接给定两张图的相对位姿尺度归一化为 1。然后按广度优先把相邻图加进来每加一张就做一次局部优化只优化新加入图及其邻居的位姿。局部优化的目标函数包含两项一是成对点图的一致性同一三维点在两图中投影应重合二是相邻图之间的位姿残差。权重用置信度的平均值。迭代个十几次就能收敛。全部图加入后再做一次全局优化把所有位姿和尺度一起调。这一步迭代次数可以少一些主要作用是消除累积误差。我个人的习惯是全局优化前后各存一份点云。对比两份的差异能看出优化到底修正了什么如果差异特别大比如某些区域整个飘了说明成对估计里有离群值没被剔掉得回去检查视图图。4.6 点云后处理与可视化优化完得到的是每张图对应的点图加上位姿。把所有点图按位姿变换到世界坐标系拼成一个整体点云。这时的点云是稠密的每张图 H×W 个点但对齐误差会让重叠区域出现“双层”现象。处理方法是做一次体素下采样体素大小按场景尺度定。室内场景 1-2 厘米比较合适室外可以放到 5-10 厘米。下采样时对每个体素内的点取平均能一定程度上缓解双层问题。可视化用 Open3D 最省事import open3d as o3d pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(all_points) pcd.colors o3d.utility.Vector3dVector(all_colors) o3d.visualization.draw_geometries([pcd])颜色直接从原图像素取因为点图和像素是一一对应的这一步没有额外成本。5. 常见问题与排查实录5.1 点云错位与“双层”结构的排查这是最常遇到的问题。现象是重叠区域出现明显的两层点像两张纸没对齐。排查顺序我总结成这样现象可能原因验证方法处理方式全局性错位参考系搞反检查点图字段是否用错用pts3d_in_other_view局部双层成对位姿有偏单独看该对点图吻合度剔除该边换路径整体尺度不一尺度未归一看不同对的平移量数量级加尺度变量优化边缘撕裂分辨率不一致检查输入尺寸统一重采样旋转错乱反射解未排除检查 R 的行列式加 det 修正项这里重点说“局部双层”。它通常不是全局优化的问题而是某一条边的成对估计本身就偏了。定位方法是把这条边单独拿出来看两张点图在重叠区域是否吻合。如果单看就不吻合说明是网络在这对上失效了可能是纹理太弱或者重叠不足这条边应该降权或者剔除。剔除边之后要检查图是否还连通。如果不连通了就得用其他边把子图接起来否则全局优化会崩。5.2 显存溢出与推理速度优化显存溢出的触发点主要有三个分辨率太高、一次塞太多对、没及时释放中间张量。分辨率是最直接的杠杆。512 长边大约 3-4 GB768 大约 7-8 GB1024 超过 10 GB。如果显存紧张优先降分辨率它对精度的影响比想象中小——从 1024 降到 512点云的整体结构基本保持只是细节变粗。批量处理时一定要手动清缓存import torch, gc del output gc.collect() torch.cuda.empty_cache()这三行看着啰嗦但在循环里能省下不少显存碎片。我试过不加跑到第二十几对的时候 OOM加了之后跑完整组没问题。速度方面单对推理在 4090 上大约 0.3-0.5 秒在 3090 上 0.6-0.8 秒。如果要处理几十张图全连接的做法会很慢必须用 top-k 邻居建边把边数控制在 2N 到 3N 之间。5.3 弱纹理与大视角跨度下的退化弱纹理场景白墙、水面、天空是这类模型的普遍软肋。表现是这些区域的置信度很低点图基本是噪声。处理思路有两条。一是降低这些区域的权重在全局优化时只用高置信度的点参与拟合低置信度区域直接排除。这样虽然这些区域没有结构但不会破坏整体对齐。二是在采集端想办法如果条件允许拍摄时刻意在弱纹理区域放一些标记物或者用带纹理的辅助板。这属于工程手段但实际很有效。大视角跨度比如相隔 120 度拍两张的问题在于重叠区域太小。这时候不要指望单对能给出好结果应该在两图之间插入中间视角构造一条链式的边让信息逐级传递。这也是为什么拍摄时建议多拍、密集采样——不是为了每个视角都用上而是为了让视图图有更多可选的边。5.4 精度评估怎么判断结果靠不靠谱没有真值的时候怎么判断结果质量我一般看三个指标成对残差每对图的点图拟合残差残差大的对说明估计不可信。重投影一致性把三维点投回各视图看重投影误差的分布长尾说明有离群点。闭环误差如果拍摄轨迹是闭合的起点和终点的位姿应该接近差值能反映累积漂移。有真值的话比如用仿真数据或者标定好的场景直接算 ATE绝对轨迹误差和点云倒角距离。我在一组仿真数据上测过成对估计的旋转误差通常在 1-2 度平移误差在 1-2% 场景尺度拼成全局后误差会累积十几张图的序列大概在 3-5 度、3-5% 的量级。这个精度对于快速预览、粗略建模够用但如果要做精密测量或者高质量渲染还得配合后端的 BA 精修或者用其他方法补强。5.5 一些容易忽略的小坑图像 EXIF 方向手机拍的图带旋转标记读进来不处理的话两张图方向不一致点图完全对不上。用 PIL 读的时候记得ImageOps.exif_transpose。色彩空间有些图像是 CMYK 或者带 alpha 通道直接转 RGB 时如果处理不当会有色偏影响特征匹配。统一转成 RGB 三通道再送进去。文件名排序批量处理时如果按字符串排序img10.jpg会排在img2.jpg前面导致视图顺序错乱。用自然排序或者按时间戳排。混合精度用 fp16 能省显存提速但点图输出的精度会降尤其是远处点的 Z 值。如果对精度敏感关键步骤用 fp32。6. 我在这套流程里攒下的几点体会跑了一段时间之后我最大的感受是DUSt3R 这类方法改变的不是精度上限而是入场门槛。以前接一个重建需求第一件事是问对方“有没有标定参数”“拍摄条件怎么样”现在可以先跑一遍看看不行再谈条件。这个顺序的变化对做项目的人来说意味着前期沟通成本大幅下降。另一个体会是关于“够用”的判断。成对点图的精度其实不算高直接拿来做测量肯定不够但用来做场景理解、粗略定位、给下游任务提供初始化完全够。我现在的用法是把它当成一个快速的几何先验生成器先用它拿到粗略位姿和点云再把这些当地初值喂给更精细的优化流程。这样比从零开始做 SfM 快很多而且对弱纹理更宽容。还有一个细节值得说这套流程里视图图的设计比模型本身更影响最终结果。模型是固定的但你怎么选边、怎么定权重、怎么安排增量顺序这些决策的空间很大。我建议把这块做成可配置的针对不同场景室内、室外、物体环绕用不同的建边策略比一味调模型参数有效。最后提一句后处理的性价比。体素下采样加统计滤波这两步代码不超过二十行但对最终点云的观感提升非常明显。很多人跑完原始点云觉得效果一般就放弃了其实差的就是这两步。花十分钟加上结果会好看很多。
阅读完成 · 觉得有帮助?