简介:这份文档面向希望深入理解或复现NeRF-SLAM的研究者与工程实践者,聚焦神经辐射场与SLAM结合的三维重建方案。内容围绕Tracking与Mapping两大阶段展开:Tracking部分讲解基于Droid SLAM与RAFT光流估计的稠密单目SLAM框架,剖析DBA稠密光束法平差层如何借助舒尔补与Cholesky分解优化相机位姿与逆深度;Mapping部分则介绍Instant-NGP哈希编码加速的概率体素化NeRF,以及深度损失与Mapping Loss在噪声深度图下的加权监督策略。作者还记录了复现过程中遇到的环境配置、依赖编译等实际问题及解决思路,对理解NeRF-SLAM整体流程与排错具有参考价值。资源为单个docx文档,压缩包约11.01MB,已有2316人学习,适合具备一定SLAM与深度学习基础、需要系统梳理该工作实现细节的读者。
1. 从稀疏点到稠密场:nerf-slam 复现文档能解决什么
如果你跑过 ORB-SLAM 这类传统视觉方案,大概都经历过这种落差:轨迹跟得挺稳,但重建出来的地图就是一堆稀疏点云,想拿来做表面重建、纹理贴图或者新视角合成,基本没法直接用。nerf-slam 这份复现文档要解决的,正是这个断层——它把 Instant-NGP 的神经辐射场和 Droid-SLAM 的稠密单目跟踪缝在一起,输入只要一段连续 RGB 图像,就能同时拿到相机轨迹和一套可以渲染的稠密辐射场。和 nice-slam 那类需要额外深度输入的工作比,它更贴近「只有单目视频」的真实场景。这份文档适合两类人:一是想上手 NeRF-based SLAM 但被环境编译卡住的工程同学,二是已经跑通传统 SLAM、想看看稠密建图到底能到什么精度的从业者。下面我按自己拆包复现的顺序,把追踪、建图、环境、坑点一层层讲清楚。
2. 追踪链路拆解:RAFT 光流、Droid-SLAM 与 DBA 层怎么串起来
nerf-slam 的追踪部分不是从零搭的,它站在 Droid-SLAM 的肩膀上,而 Droid-SLAM 又借了 RAFT 的光流估计思路。理解这条链路,是后面调参和排错的前提——不然你看到 convGRU 输出一堆残差量,根本不知道它在优化什么。
2.1 RAFT 的三个组件与 4D 相关体
RAFT 做光流估计的核心,是把「找对应点」这件事变成一个可微的迭代查表过程。它由三块组成:特征编码器从两张输入图里各提一套逐像素特征,上下文编码器只从第一张图提特征;相关层把两套特征做所有对的内积,构建出一个 4D 的 W×H×W×H correlation volume;更新算子则拿着当前光流估计,从这个 4D 体里反复查值来修正。
这里的关键参数是相关体的多尺度合并。4D 体的最后两维会在多个尺度上做池化,形成一组多尺度体积。为什么要多尺度?因为大位移的光流在小尺度上更容易被捕获,而细节修正要靠大尺度。我一般会把尺度数当成一个隐性超参来看——尺度太少,快速运动场景容易丢;尺度太多,显存和查表开销都上去了。
# RAFT 相关体构建的简化逻辑(示意,非项目源码) import torch import torch.nn.functional as F def build_correlation_volume(fmap1, fmap2, num_levels=4): # fmap1, fmap2: [B, C, H, W] B, C, H, W = fmap1.shape # 所有对的内积 -> [B, H*W, H*W] corr = torch.einsum('bchw,bdhw->bcd', fmap1, fmap2) # 简化示意 corr = corr.view(B, H, W, H, W) # 4D correlation volume volumes = [corr] for _ in range(num_levels - 1): # 最后两维做平均池化,构建多尺度体积 corr = F.avg_pool2d(corr.view(B, H, W, H*W), 2).view(B, H//2, W//2, -1) volumes.append(corr) return volumes上面这段是帮助理解结构的示意代码,不是项目里能直接跑的模块。逻辑说明:einsum那一步就是在算所有像素对的特征内积,得到 4D 体;后面的循环对最后两维做池化,得到多尺度版本。参数说明:num_levels控制尺度数量,实际项目里这个值藏在 RAFT 的配置里,改它要同步检查显存占用。注意,真正的 Droid-SLAM 并不是直接拿 RAFT 原版,而是改了更新部分,把 convGRU 的输出接到 frame graph 的边上。
2.2 Droid-SLAM 的 frame graph 与残差量
Droid-SLAM 在 RAFT 基础上改的地方,是让更新算子作用在 frame graph 的边上。每个边连接两帧,预测出来的东西经过 DBA 层映射成深度和位姿的修正。这里有几个符号要认清:pi 是 frame i 上的像素点,di 是 frame i 的逆深度,pij 表示把 pi 用估计的位姿和深度投影到 frame j 后的位置。Cij 就是拿 pij 去 correlation volume 里查表得到的值。
convGRU 在这里不直接输出位姿和深度的残差,而是输出 pij 的残差量 rij 和信心矩阵 wij。这个设计挺巧妙——它把「我该往哪修正」和「我有多确信」分开表达,信心矩阵后面会参与加权,噪声大的区域自然被压低。我踩过的一个坑是:如果信心矩阵初始化不对,前几帧的位姿会抖得厉害,后面虽然能收敛,但轨迹开头会有一段漂移。
2.3 DBA 层里的舒尔补与 Cholesky 分解
DBA(Dense Bundle Adjustment Layer)是整个追踪里最像传统 SLAM 的部分。BA 的目标是同时优化相机位姿和路标点,但路标点数量远大于相机数量,直接解大 H 矩阵不现实。常见做法是用舒尔补(Schur complement)把问题降维:先消掉路标点相关的块,得到一个只和相机相关的 Ht,解出相机位姿增量,再回代求逆深度增量。
文档里提到用 Cholesky 分解 H = LLT 来求解,这是数值上的常规操作。边际协方差 ΣT 和 Σd 也是在这个过程中顺带算出来的,它们会作为不确定性权重传给后面的 NeRF 监督。参数上要留意的是:舒尔补的顺序不能随便换,先相机后路标是标准做法;如果反过来,矩阵条件数会变差,解出来的位姿容易跳。
# DBA 中舒尔补求解的简化示意 import numpy as np def schur_complement_solve(H, b, num_cam, num_point): # H: 完整 Hessian,b: 残差向量 Hcc = H[:num_cam, :num_cam] Hcp = H[:num_cam, num_cam:] Hpc = H[num_cam:, :num_cam] Hpp = H[num_cam:, num_cam:] bc = b[:num_cam] bp = b[num_cam:] # 舒尔补:消掉路标点块 S = Hcc - Hcp @ np.linalg.inv(Hpp) @ Hpc rhs = bc - Hcp @ np.linalg.inv(Hpp) @ bp # Cholesky 分解求解相机增量 L = np.linalg.cholesky(S) delta_cam = np.linalg.solve(L.T, np.linalg.solve(L, rhs)) # 回代求路标点增量 delta_point = np.linalg.solve(Hpp, bp - Hpc @ delta_cam) return delta_cam, delta_point逻辑说明:先分块,再用Hpp的逆把路标点消掉,得到只含相机的S,Cholesky 分解后解出delta_cam,最后回代。参数说明:num_cam和num_point决定分块边界,实际项目里这两个数随帧数动态变化。注意,真实实现不会直接求Hpp的逆,而是用分解或稀疏求解器,这里只是为了把数学过程写清楚。
3. 建图侧:Instant-NGP 哈希编码与概率体素 NeRF 的落地
追踪给出位姿和带协方差的深度,建图侧要做的就是把这些监督信号喂给 NeRF,让它学出一个可以渲染的稠密场。nerf-slam 用的是 Instant-NGP 那套哈希编码,速度比原始 NeRF 快很多,这也是它能在线跑起来的关键。
3.1 哈希编码为什么能把 NeRF 拉快
原始 NeRF 把位置编码成高频正弦函数,MLP 要学很久才能收敛到细节。Instant-NGP 换了个思路:把空间划分成多分辨率网格,每个网格顶点存一组可学习的特征向量,查询时按位置做插值。但网格数量随分辨率指数增长,显存扛不住,所以它用哈希表来存——多个网格顶点共享同一个哈希桶,冲突了就让 MLP 去解耦。
文档里以 L=2 两层分辨率为例:对应点 x 在粉色和蓝色两层分别有不同的哈希编码值,把它们和输入编码拼接后一起送进 MLP。这里要调的参数主要是哈希表大小和每层特征维度。哈希表太小,冲突严重,重建会糊;太大,显存吃紧。我一般会先按默认值跑通,再根据显存余量往上加。
# 编译 instant-ngp 前先确认 cmake 版本 cmake --version # 如果低于 3.22,需要升级,文档里给了 3.22 / 3.26 的安装步骤 # 进入 nerf-slam 目录后编译 ngp cmake ./thirdparty/instant-ngp -B build_ngp cmake --build build_ngp --config RelWithDebInfo -j逻辑说明:这两条命令先配置再编译,-B build_ngp指定构建目录,--config RelWithDebInfo是带调试信息的发布模式,-j开多核。参数说明:如果报Could NOT find GLEW,说明缺 OpenGL 扩展库,装libglew-dev再重跑 cmake 即可。注意,cmake 版本不够是这一步最常见的翻车点,文档里专门列了升级流程不是没道理的。
3.2 深度损失与 λD 的加权逻辑
建图的损失函数同时监督颜色和深度,用一个超参 λD 来平衡,文档里把 λD 设为 1.0。深度损失的特殊之处在于:它不是直接拿深度图做 L2,而是用边缘协方差 ΣD 做加权。密集单目 SLAM 估计出来的深度,在无纹理或锯齿区域噪声很大,如果不加权,这些坏点会把辐射场带偏。
加权的形式可以理解为:协方差大的地方,损失权重小;协方差小的地方,权重大。这样即使深度图整体有噪声,只要不确定性估计是合理的,辐射场依然能收敛。文档里那组对比图很说明问题——有噪声位姿加无深度,60 秒内不收敛;有真值深度加噪声位姿,结果很好;而他们的方法用噪声位姿加噪声深度,靠不确定性加权,也能拿到接近真值深度的效果。
| 输入条件 | 收敛情况 | 说明 |
|---|---|---|
| 真值位姿 + 真值深度 | 快速准确 | 理想情况,用于验证上限 |
| 真值位姿 + 无深度 | 较慢但能收敛 | 经典 NeRF 输入模式 |
| 噪声位姿 + 无深度 | 60 秒内不收敛 | 最差情况,说明位姿噪声影响大 |
| 噪声位姿 + 真值深度 | 结果良好 | 深度监督能补偿位姿噪声 |
| 噪声位姿 + 噪声深度 + 协方差加权 | 结果良好 | 本文方法的目标场景 |
这张表是我根据文档描述整理的,方便对照理解每种输入组合的边界。实际调的时候,如果你发现重建一直不收敛,先检查深度协方差是不是全为常数——如果是,加权就退化成均匀权重,噪声压不住。
3.3 从 RGB 到辐射场的完整数据流
把追踪和建图串起来看,数据流是这样的:连续单目图像进来,Droid-SLAM 估计位姿和逆深度,同时给出边缘协方差;这些量作为监督信号送进 Instant-NGP 的 NeRF,颜色损失和加权深度损失一起反传,更新哈希特征和 MLP 权重。整个过程是交替进行的,不是先跑完 SLAM 再跑 NeRF。
这里有个容易忽略的点:NeRF 的深度监督来自 SLAM 估计的深度图,而 SLAM 的位姿又会被 NeRF 的梯度间接影响。如果两者更新频率不匹配,比如 NeRF 更新太快而 SLAM 还没稳定,就会出现震荡。常见做法是让 SLAM 先跑几帧稳定下来,再开启 NeRF 的联合优化。
4. 环境搭建避坑:cmake、Boost、GTSAM 的连环依赖
这一章是我复现时花时间最多的地方。nerf-slam 的依赖链条比较长,cmake、Boost、GTSAM、instant-ngp 任何一个版本不对,都会在编译阶段卡住。下面按我实际遇到的顺序,把几个高频坑点列出来。
4.1 cmake 版本不够导致 ngp 编译失败
现象:执行cmake ./thirdparty/instant-ngp -B build_ngp时直接报版本错误,或者编译到一半提示需要更高版本。
原因:instant-ngp 要求 cmake > 3.22,而很多系统自带的是 3.10 或 3.16。
解决:按文档里的流程升级。可以先下 3.22 的预编译包,解压后sudo make install,再hash -r刷新命令缓存,最后cmake --version确认。如果系统里有多版本共存,注意hash -r这一步不能省,否则 shell 还在用旧路径。
wget https://cmake.org/files/v3.22/cmake-3.22.0-rc2-linux-x86_64.tar.gz --no-check-certificate tar -zxvf cmake-3.22.0-rc2-linux-x86_64.tar.gz cd cmake-3.22.0-rc2-linux-x86_64/ sudo make install hash -r cmake --version逻辑说明:下载预编译包、解压、安装、刷新缓存、验证。参数说明:--no-check-certificate是跳过证书检查,内网环境常用;hash -r清掉 shell 对旧 cmake 路径的缓存。注意,如果你之前用 apt 装过 cmake,make install后可能还是旧版本优先,需要检查 PATH 顺序。
4.2 GLEW 缺失导致 cmake 配置中断
现象:cmake 配置 instant-ngp 时报Could NOT find GLEW (missing: GLEW_INCLUDE_DIRS GLEW_LIBRARIES)。
原因:系统没装 OpenGL 扩展加载库,instant-ngp 的可视化部分依赖它。
解决:sudo apt install libglew-dev,然后重新跑 cmake 配置和编译。这个坑比较直接,装完就好,但要注意装完后最好把 build 目录清掉重来,避免缓存了失败的配置。
4.3 Boost 版本不满足 GTSAM 要求
现象:配置 GTSAM 时报Could NOT find Boost (missing: Boost_INCLUDE_DIR serialization system filesystem thread program_options date_time timer chrono regex) (Required is at least version "1.65")。
原因:系统自带 Boost 版本太低,或者只装了部分组件,缺 serialization、filesystem 这些库。
解决:从源码编译安装 Boost 1.73.0。文档里给的流程是先bootstrap.sh,再./b2 --buildtype=complete install。我一般会选完全安装,虽然慢一点,但省得后面缺库再回来补。如果系统里有多个 Python 版本,记得用--with-python=python*指定,否则 Boost.Python 可能绑到错误的解释器上。
wget https://boostorg.jfrog.io/artifactory/main/release/1.73.0/source/boost_1_73_0.tar.bz2 --no-check-certificate tar xvf boost_1_73_0.tar.bz2 cd boost_1_73_0 ./bootstrap.sh sudo ./b2 --buildtype=complete install逻辑说明:下载、解压、生成构建脚本、完整编译安装。参数说明:--buildtype=complete表示编译所有组件;默认安装到/usr/local/lib,如果想指定路径,加--prefix=xxx,但之后要手动配环境变量。注意,Boost 编译时间不短,建议开-j加速,但b2的并行参数写法是-jN,别和 make 搞混。
4.4 GTSAM Python 包装器编译失败
现象:cmake ./thirdparty/gtsam -DGTSAM_BUILD_PYTHON=1 -B build_gtsam报错,或者编译出来的 Python 模块 import 失败。
原因:Boost 没装好是最常见的,其次是 Python 版本和 GTSAM 期望的不一致。
解决:先确认 Boost 装完且能被 cmake 找到,再检查python --version和 GTSAM 的兼容性。如果 import 时报符号未定义,多半是 Boost.Python 的 ABI 问题,重新编译 Boost 时指定正确的 Python 版本通常能解决。
提示:这一章的依赖问题有很强的顺序性,建议按 cmake → GLEW → Boost → GTSAM → instant-ngp 的顺序逐个解决,不要跳步,否则错误信息会互相掩盖。
5. 复现后的验证与调参:怎么判断跑对了
环境搭好、代码跑起来之后,真正的挑战是判断结果对不对。NeRF-based SLAM 不像传统 SLAM 有明确的轨迹误差指标,它的输出是一个辐射场,好坏需要从渲染质量和轨迹一致性两个维度看。
5.1 用渲染质量反推追踪是否稳定
最直接的验证方式是:跑完一段序列后,从训练视角之外渲染几张图,看有没有明显的模糊、鬼影或漂浮物。如果渲染图在边缘区域糊成一片,通常是追踪位姿漂了,导致 NeRF 学到的几何不一致。这时候可以回看追踪阶段的信心矩阵,如果某些帧的信心值普遍偏低,说明光流估计在那几帧不可靠。
我一般会固定一个检查流程:先渲染训练视角,确认颜色和深度对齐;再渲染几个插值视角,看几何是否连贯;最后对比深度图和 SLAM 输出的深度,看协方差大的区域是不是真的对应噪声大的地方。这个流程走一遍,基本能定位问题出在追踪还是建图。
5.2 λD 和哈希表大小的调参边界
λD 控制深度监督的权重,文档里设的是 1.0。如果你的场景纹理丰富、深度估计比较准,可以适当调大,让几何更准;如果场景无纹理区域多,深度噪声大,调小一点让颜色监督占主导,避免被坏深度带偏。但别调得太极端,λD 太大容易过拟合深度噪声,太小则几何松散。
哈希表大小是另一个关键参数。太小,重建细节丢失,渲染出来像蒙了一层雾;太大,显存爆掉。我的经验是先用默认值跑通,看显存占用,如果还有余量就往上加一档,直到渲染质量不再明显提升为止。注意,哈希表大小和分辨率层数是耦合的,改一个要同步看另一个。
5.3 从失败案例里读懂的边界
文档里那组对比实验其实给了很清晰的边界:没有深度监督时,纯靠颜色,NeRF 在噪声位姿下 60 秒内不收敛。这说明位姿噪声是比深度噪声更致命的问题。如果你的复现一直不收敛,优先检查追踪位姿的质量,而不是急着调 NeRF 的参数。
另一个边界是:这套方法假设深度不确定性可以被合理估计。如果 SLAM 给出的协方差全是常数或者量级不对,加权就失效了。我踩过一次坑,协方差输出全为零,结果深度损失权重爆炸,辐射场直接崩掉。从那以后我每次跑新序列,都会先打印几帧协方差的统计量,确认它不是退化的。
6. 一个具体技巧:用协方差统计快速定位退化帧
最后分享一个我复现时总结出来的小技巧,能帮你快速定位哪些帧的追踪出了问题。核心思路是:把每帧的深度边缘协方差做统计,看它的均值和方差随时间的变化。正常帧的协方差应该在一个合理范围内波动,如果某一帧突然飙高或者掉到接近零,那帧大概率有问题。
import numpy as np def check_covariance_health(cov_list, window=10): # cov_list: 每帧的深度边缘协方差图,展平后的数组列表 means = [np.mean(c) for c in cov_list] stds = [np.std(c) for c in cov_list] for i, (m, s) in enumerate(zip(means, stds)): # 用滑动窗口的均值做参考,偏离过大就标记 if i >= window: ref = np.mean(means[i-window:i]) if m > 3 * ref or m < 0.1 * ref: print(f"帧 {i} 协方差异常: mean={m:.4f}, ref={ref:.4f}") return means, stds逻辑说明:对每帧协方差图算均值和标准差,用前一个窗口的均值做参考,偏离超过 3 倍或低于 0.1 倍就标记为异常。参数说明:window控制参考窗口大小,太小容易误报,太大则反应迟钝,10 帧左右是个比较稳的起点。注意,这个脚本只是辅助定位,标记出来的帧不一定真的错,但值得回看它的光流和位姿。
我现在的习惯是:每次跑新序列,先跑一遍协方差健康检查,把异常帧挑出来,再决定是整段重跑还是只处理问题帧。这个习惯帮我省了不少盲目调参的时间。从那以后我每次复现 NeRF-based SLAM,都强制走一遍协方差统计,确认追踪没退化再去看建图效果。希望帮到你。
本文还有配套的精品资源,点击获取