简介:围绕NeRF-SLAM复现整理的docx文档,面向已具备视觉SLAM与深度学习基础、希望动手实现稠密三维重建的研究生和算法工程师。压缩包内仅1个docx文件,大小约11.01MB,内容集中,便于按主题通读,已有2326人学习。文档先对比传统稀疏关键点SLAM与NeRF稠密建图的差异,再按追踪与建图两条主线展开:追踪部分梳理基于协方差的稠密SLAM、Instant-NGP深度监督与边缘协方差加权,并延伸到RAFT光流、Droid SLAM及稠密BA层,说明舒尔补与Cholesky分解如何求解相机位姿和逆深度;建图部分讨论概率体素化NeRF、哈希编码加速、建图损失及深度损失设计。作者还记录了复现中遇到的问题与处理思路,对复现路线、关键公式和排错线索均有交代,可帮助读者理解实验配置、模块衔接与调参方向,适合作为论文阅读和工程复现之间的参考材料。
1. 单目视频喂进去,稠密地图长出来
我第一次把一段手机拍的单目视频喂进 nerf-slam 的复现流程时,最反直觉的地方在于:跟踪前端根本不需要深度相机。整条链路只吃连续 RGB 图像,位姿和逆深度由 Droid-SLAM 这套稠密光流方案估出来,稠密的三维重建结果则交给 Instant-NGP 在后台一点点"长"出来。它适合两类人:一类是想把 NeRF 从"摆拍式重建"推到"边走边建"的算法同学,另一类是手里有视频、想拿到可渲染稠密地图的工程同学。这份复现文档的价值不在论文推导,而在于把 RAFT 光流、DBA 舒尔补求解、hash 编码体渲染这条长链路,连同 cmake、glew、boost、gtsam 一堆编译依赖,全部踩过一遍并记了下来——新手照着能跑通,熟手能直接看架构取舍。
2. 架构拆解:一条 RGB 输入如何被两套网络分食
2.1 从 Img1/Img2 到稠密地图的完整数据流
整个系统的输入是连续单目图像序列,这里记作 Img1、Img2,以及它们之间的时间戳。数据流的起点是一对相邻帧:先经过 RAFT 风格的特征编码器提取逐像素特征,构建 4D correlation volume;再由 Droid-SLAM 的 convGRU 迭代更新,输出像素级残差和置信度;这些量进入 DBA 层,被映射成相机位姿修正和逆深度修正。到这里,跟踪线程交出的是一串位姿 T 和一张逐像素逆深度图 D。
关键在下一步:这张 D 不是最终产物,而是作为监督信号送给 Instant-NGP。NeRF 一侧同时接收 RGB 图像 I 和深度 D,深度项由 Droid 算出的边缘协方差 ΣD 加权,颜色项照常做体渲染的光度监督。两路损失一起反传,更新的是哈希网格和 MLP 的神经参数 θ。
原文那张架构图里,蓝色是 Droid-SLAM 的贡献,粉色是 Rosinol 那条概率体素路线的贡献,红色是这份工作自己加的部分。颜色分得清楚,其实就是在说:跟踪和建图不是耦合在一起联合优化的,而是"跟踪算完 → 协方差传下去 → 建图带权监督"的串联结构。这种松耦合的好处是每一段都能单独替换、单独调试,坏处是跟踪崩了,后面的建图再准也救不回来。
2.2 三个上游工作的分工:RAFT、Droid-SLAM、Instant-NGP
把依赖拆开看,这个项目其实站在三个巨人肩上,每个负责一段职责,边界很清楚:
| 组件 | 负责的事 | 输出的东西 | 在本项目里的角色 |
|---|---|---|---|
| RAFT | 光流估计,提供 correlation volume 与迭代更新范式 | 多尺度 4D 相关体、光流增量 | 提供前端匹配的"骨架" |
| Droid-SLAM | 把光流更新改造成位姿/深度更新,接 DBA 层 | 位姿 T、逆深度 d、协方差 ΣD | 跟踪线程主体 |
| Instant-NGP | hash 编码加速的 NeRF,快速拟合辐射场 | 颜色、深度、体密度 | 建图线程主体 |
RAFT 由三个主要组件构成:特征编码器从两帧里各提一次逐像素特征,上下文编码器只从 I1 提特征;相关层把所有像素对做内积,堆出一个 4D 的 W×H×W×H correlation volume;更新算子拿当前光流估计去这组相关体里查表,反复迭代细化。最后那个 4D 体的后两维会在多个尺度上做池化,形成一组多尺度体积,这是为了兼顾大位移和小细节。
Droid-SLAM 改的就是"更新"这一段。RAFT 的 convGRU 直接回归光流,Droid 则让更新算子作用在 frame graph 的边上,输出的东西经 DBA 层映射成深度和位姿的修正量。这个改动看着小,实际是把一个 2D 匹配问题抬成了 3D 几何优化问题,也是整个方案能脱离深度真值工作的根本原因。
Instant-NGP 那边负责的是速度。传统 NeRF 的 MLP 要查位置编码,收敛慢;hash 编码把空间切成多层网格,每个网格顶点存可学习特征,查表比过 MLP 便宜得多。原文以 L=2 两层分辨率举例:同一个空间点 x,在粉色层和蓝色层分别查到不同的 hash 编码值,把这些特征拼起来再送进一个小 MLP,输出密度和颜色。
2.3 与 nice-slam 的关键差别:只要 RGB
同样是把 NeRF 塞进 SLAM,nice-slam 那条路线要求输入带深度,而这里只需要 RGB。差别不在"能不能省一个传感器",而在监督信号的来源变了:深度不再来自传感器,而是来自 Droid-SLAM 的稠密估计,并且这个估计自带不确定性——边缘区域方差大,无纹理区域方差更大。
所以整篇文章里最值得盯住的设计,是"协方差"这个东西贯穿了始终。它在 DBA 里是 Hessian 逆的对角块,到了建图侧就变成深度损失的逐像素权重。原文那张对比图说得很直白:给真值位姿和真值深度,收敛最快最准;只给真值位姿不给深度,也能收敛,只是慢;反过来给噪声位姿又没有深度,60 秒内根本收不住;而给真值深度加噪声位姿,结果依然不错。这组实验其实是在论证一件事——只要按不确定性加权,噪声深度也扛得住。这正是这份工作想要达到的工况。
2.4 仓库结构与子模块分布
工程上这是个典型的"主仓库 + 子模块"结构,两个大头都在 thirdparty 下:thirdparty/instant-ngp是 NeRF 那一半,thirdparty/gtsam是因子图优化那一半,gtsam 还要单独编出 Python wrapper 才能在训练脚本里 import。requirements 也分两份,一份是主工程的,一份在thirdparty/gtsam/python/requirements.txt,装漏了会在 import 阶段才报错。
# 目录结构示意(按复现时的实际布局整理) nerf-slam/ ├── thirdparty/ │ ├── instant-ngp/ # hash 编码 NeRF,需要 cmake 编译 │ └── gtsam/ # 因子图优化,需开 GTSAM_BUILD_PYTHON ├── requirements.txt # 主工程 Python 依赖 └── ... # 跟踪 / 建图 / 数据接口脚本提示:clone 时如果忘了
--recurse-submodules,thirdparty 下会是两个空目录,后面 cmake 报的错会非常迷惑,先ls thirdparty确认不是空的再往下走。
3. 核心实现拆解:RAFT 光流、DBA 舒尔补与深度协方差
3.1 correlation volume:4D 相关体怎么建、怎么查
RAFT 里最吃显存也最关键的一步,是把两帧特征做全对内积。假设特征图是 H×W,通道 C,那么 I1 的每个像素和 I2 的每个像素都要算一次相似度,结果就是一个 H×W×H×W 的四维体。直接算会爆显存,所以工程上都会先降分辨率、限制搜索半径。
import torch import torch.nn.functional as F def build_correlation_volume(fmap1, fmap2): """fmap1/fmap2: [B, C, H, W],已归一化到单位长度""" b, c, h, w = fmap1.shape # 展平成 [B, C, H*W],做批量矩阵乘,得到 [B, H*W, H*W] f1 = fmap1.flatten(2) # [B, C, H*W] f2 = fmap2.flatten(2) # [B, C, H*W] corr = torch.bmm(f1.transpose(1, 2), f2) # [B, H*W, H*W] corr = corr.view(b, h, w, h, w) # 还原成 4D 相关体 # 最后两维做多尺度池化,得到一组金字塔体积 volumes = [corr] for _ in range(3): corr = F.avg_pool2d(corr.view(b, h*w, h, w), 2, 2).view(b, h, w, h//2, w//2) volumes.append(corr) return volumes这段代码的关键点有三个。torch.bmm那一步是把 4D 相关体压成两次矩阵乘,避免 Python 层双循环;view还原维度是为了后续按像素坐标索引;多尺度池化是为了让更新算子在粗层抓大位移、在细层抠亚像素,RAFT 原文用的就是这套金字塔。
实际跑的时候我一般会先降到 1/8 分辨率算相关体,再在查询时用双线性插值采回原尺度。全分辨率建一次 4D 体,显存占用是平方级增长的,单卡很容易直接 OOM,这一点在避坑章节还会再提。
3.2 Droid-SLAM 的改造:convGRU 输出的是残差和置信度
Droid-SLAM 在 RAFT 基础上的改动集中在更新算子的输出语义上。看那张 frame graph 的图:算子作用在图的边上,每条边对应一对帧 (i, j)。这里的 p 表示 frame i 上的像素点,di 是它的逆深度,通过相机投影和反投影,p 用估计的位姿和深度映射到 frame j 上,得到 pij。Cij 则是用 pij 去 correlation volume 里查表拿到的特征,图里记作 Lr。
convGRU 并不直接吐位姿和深度的残差,它吐的是 pij 的残差 rij 和置信度 wij。这个设计的好处是把"网络输出"和"几何量"解耦了:网络只负责说"这个像素应该往哪挪、我有多确定",剩下怎么把这个残差变成位姿修正,交给后面的 DBA 用几何模型处理。
# 更新算子的伪代码:迭代 N 次,每次用当前估计去相关体查表 for it in range(num_iterations): # 用当前位姿 T 和逆深度 d 把 p_i 投到 frame j,得到 p_ij p_ij = project(T_ij, p_i, d_i) # 在相关体里按 p_ij 双线性采样,得到 Lr corr_feat = sample_correlation(correlation_volumes, p_ij) # convGRU 输出像素残差 r_ij 与置信度 w_ij r_ij, w_ij, hidden = update_operator(corr_feat, hidden, context) # 残差回代到逆深度和位姿的增量上,交给 DBA 求解 d_i, T_ij = apply_residual(p_ij, r_ij, d_i, T_ij)参数上,迭代次数是精度和耗时的直接权衡,原文没给具体值,常见做法是训练阶段 8~12 次、推理阶段可以降到 4~6 次。置信度 wij 后面会参与加权,低置信度的边在优化里几乎不起作用,这相当于给网络一个"我这条边不可信,别听我的"的出口。
3.3 DBA 层:舒尔补把 H 矩阵切成相机块和深度块
Dense Bundle Adjustment Layer 是整个跟踪链路里最"传统 SLAM"的一块,也是最容易看晕的一块。先把符号对齐:c 和 p 分别表示 BA 里的相机和路标点,p 的数量远远大于 c,所以求解正规方程 H·Δ = b 时不会直接求逆,而是先用舒尔补把路标点消掉。
设待求量是相机位姿增量 Δξ 和路标点逆深度增量 Δd,H 按这个顺序分块:
import torch def dba_solve(H_cc, H_cd, H_dd, b_c, b_d): """稠密 BA 的舒尔补求解 H_cc: 相机-相机块, 尺寸 6N x 6N H_cd: 相机-深度块, 尺寸 6N x M H_dd: 深度-深度块, 对角块, M x M b_c / b_d: 对应右端项 """ # 第一步:H_dd 是对角块,求逆极便宜,先算 H_dd^{-1} * b_d inv_dd_bd = b_d / H_dd_diag # 逐元素除,M 维 # 第二步:舒尔补 S = H_cc - H_cd @ H_dd^{-1} @ H_cd^T S = H_cc - H_cd @ (H_cd.T / H_dd_diag.unsqueeze(0)) # 第三步:解 S * Δξ = b_c - H_cd @ H_dd^{-1} @ b_d rhs = b_c - H_cd @ inv_dd_bd delta_xi = torch.linalg.solve(S, rhs) # 内部走 Cholesky: S = L L^T # 第四步:回代解出逆深度增量 delta_d = (b_d - H_cd.T @ delta_xi) / H_dd_diag return delta_xi, delta_d逐段说逻辑。第一步之所以能逐元素除,是因为 H_dd 本身是对角的——每个像素的逆深度只和自己耦合,不跨像素,这个近似在稠密 SLAM 里几乎是标配做法,换来了极大的稀疏性。第二步构造舒尔补 S,本质是把 M 维的深度未知量边缘化掉,得到一个只有 6N 维的小系统,N 通常是个位数到十几,解起来毫无压力。第三步的torch.linalg.solve内部会走 Cholesky 分解,也就是原文里写的 H = L·Lᵀ,这一步既解了方程,又顺手拿到了矩阵分解。
真正要留意的是第四步之后那件事:边际协方差。ΣT 和 Σd 分别是相机位姿和逆深度的边际协方差,它们可以从 Cholesky 因子里回代得到——具体就是用 L 做两次三角回代,取对应块的对角线。Σd 就是后面建图侧深度损失的逐像素权重,这条线把跟踪和建图串起来了,不是随便加的一个权重项。
还有一个实现细节值得单独讲:Cholesky 分解只对对称正定矩阵成立,S 在数值上必须是正定的。如果某一帧的观测太少、或者协方差被噪点污染,S 可能退化成半正定,torch.linalg.solve就会抛错或者给出离谱的解。常见做法是给对角线加一个极小的阻尼项 λI,这个 λ 一般取 1e-6 到 1e-4 之间,我一般从 1e-5 起调。
3.4 深度协方差怎么进损失:ΣD 的加权方式
跟踪线程算完,协方差 ΣD 会跟着深度图 D 一起送到建图侧。原文的深度损失写作对姿态 T 和神经参数 θ 的联合最小化,超参 λD 用来平衡深度监督和颜色监督,作者取的是 1.0。
# 建图侧的深度损失:用协方差 ΣD 加权,压掉噪声像素的影响 depth_pred = rendered_depth # NeRF 体渲染出来的深度 depth_gt = tracked_depth # Droid-SLAM 估计的深度 sigma_d = depth_covariance # 来自 Cholesky 回代的逐像素方差 # 方差越大权重越小;加 eps 防止除零 weight = 1.0 / (sigma_d + 1e-6) depth_loss = (weight * (depth_pred - depth_gt).abs()).mean() total_loss = lambda_d * depth_loss + color_loss # lambda_d = 1.0这里的逻辑就一句话:不确定的像素少听它的。无纹理区域、物体边缘、遮挡边界这些地方,Droid 估出来的深度本身就不可靠,硬拿它当监督只会把辐射场带偏。用 1/σ 加权之后,可靠像素主导梯度,不可靠像素基本被压成噪声。
从原文的消融也能反推这个设计的必要性:给真值姿态和深度是最理想情况,收敛快且准;只给真值姿态、没有深度,靠 RGB 一样能收敛,只是慢——这就是经典 NeRF 的 pose-image 输入;而给噪声姿态又没深度,60 秒内压根不收敛。整套工作的目标工况是"噪声姿态 + 噪声深度",靠的就是这个加权。
注意:λD 设成 1.0 是原文给的经验值,但如果换到自己采的数据上,深度噪声明显更大,这个值往下调到 0.3~0.5 通常更稳;反过来如果你的深度来自比较准的传感器,可以往上抬到 2.0 左右观察收敛速度。
4. 环境搭建:从 cmake 3.22 到 gtsam Python wrapper
4.1 依赖基线:虚拟环境、torch 与两套 requirements
这一套依赖对版本很挑,尤其是 torch 和 CUDA 的配对。复现文档里用的组合是 CUDA 11.3 配 torch 1.12.1,装的时候要带+cu113后缀,不然 pip 会给你拉一个 CPU 版本,后面编译 instant-ngp 时链接一塌糊涂。
# 带子模块克隆,别漏 --recurse-submodules git clone <repo> --recurse-submodules cd nerf-slam git submodule update --init --recursive # 建虚拟环境(原文强调从这一步开始都待在 venv 里) python -m venv venv source venv/bin/activate # CUDA 11.3 对应版本的 torch pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 \ --extra-index-url https://download.pytorch.org/whl/cu113 # 两份 requirements 都要装,gtsam 那份容易漏 pip install -r requirements.txt pip install -r ./thirdparty/gtsam/python/requirements.txt命令本身没什么技术含量,但顺序有讲究。torch 必须先装且装对版本,因为后面编译 gtsam 的 Python wrapper 时会去链接当前环境的 Python 和 numpy 头文件,如果你中途换过环境或者装了两份 numpy,wrapper 编出来 import 就会失败。我一般会在pip install之前先python -c "import torch; print(torch.version.cuda)"确认一遍。
4.2 编译 instant-ngp:cmake 版本、GLEW 与一条构建命令
instant-ngp 这一半要求 cmake 大于 3.22,这是硬门槛。Ubuntu 自带的 cmake 往往是 3.16 上下,直接 cmake 过去会报"CMake 版本过低"或者某些语法不认识。升级方式有源码编译和直接下二进制的预编译包两条路,复现文档里走的是源码/预编译包混合的一条路径,大致就是下载、解压、sudo make install三步。
# 先看当前版本 cmake --version # 下载 3.22 预编译包并安装(需要 cmake>3.22 才能配 instant-ngp) 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 cp -r bin/* /usr/local/bin/ && sudo cp -r share/* /usr/local/share/ # 装完一定要刷 hash,否则 shell 还在用旧路径 hash -r cmake --version # 配置并编译 ngp cd nerf-slam cmake ./thirdparty/instant-ngp -B build_ngp cmake --build build_ngp --config RelWithDebInfo -j第一次 cmake 大概率会停在 GLEW 上:Could NOT find GLEW (missing: GLEW_INCLUDE_DIRS GLEW_LIBRARIES)。这个不是 cmake 的问题,是系统缺 OpenGL 扩展加载库,sudo apt install libglew-dev装完重新 cmake 就行。类似的还会缺 GLFW、X11 相关开发包,报错信息里写什么就装什么,这一步没什么玄学。
--config RelWithDebInfo这个参数值得说一下:它同时开优化和调试符号,是 instant-ngp 官方推荐的构建类型。用 Debug 会慢到无法接受,用纯 Release 崩溃时给的栈没法看。-j后面跟核数,机器内存不够的话并发太高会在链接阶段被 OOM killer 干掉,8G 内存的机器建议-j4。
4.3 编译 gtsam 与 Boost 1.73:源码安装与路径问题
gtsam 是因子图优化库,本项目要用它的 Python wrapper,所以配置时必须带-DGTSAM_BUILD_PYTHON=1。
cmake ./thirdparty/gtsam -DGTSAM_BUILD_PYTHON=1 -B build_gtsam这一步最常见的失败是 Boost: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,要么版本低于 1.65,要么装了但 cmake 找不到头文件路径。稳妥的做法是源码装一个 1.73。
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 sudo ldconfig # 刷新动态库缓存,否则运行时找不到 libboost_*--buildtype=complete是全部编译安装,图省事就这么干,代价是编译时间长。Boost 默认装到/usr/local/lib,如果系统里已经有一份 apt 装的旧版 Boost 在/usr/lib,链接时可能优先命中旧的,这时候要靠ldconfig的顺序或者显式指定BOOST_ROOT来兜底。多 Python 版本共存时,./bootstrap.sh --with-python=python3.8这种写法能指定绑定的解释器,不指定就用系统默认。
Boost 装好后回到 gtsam 的 cmake 命令重跑一遍,配置通过再cmake --build build_gtsam -j。编出来的 Python 模块通常落在build_gtsam/python/下,需要把这一层加进PYTHONPATH才能import gtsam。这点原文档里没细说,但漏了这一步会在跑训练脚本时直接 ImportError,很难往编译问题上联想。
4.4 数据组织与第一次启动
数据这块,跟踪线程要的是连续单目图像序列,目录结构按帧号顺排即可,位姿真值只在评估阶段用得到。启动前建议先确认三件事:build_ngp下编出了可执行文件、python -c "import gtsam"不报错、显存至少有 8G 空闲。
# 启动前自检三连 ls build_ngp/ | head python -c "import gtsam; print(gtsam.__file__)" nvidia-smi --query-gpu=memory.free --format=csv # 典型启动方式:指定数据目录与输出目录 python run.py --data_root ./data/scene_01 --out_dir ./logs/scene_01首次跑建议只用一小段序列,比如 100~200 帧,先把跟踪位姿和深度图存下来看趋势对不对,再放到全长序列上。整条链路第一次跑通的时间成本主要花在编译上,运行本身反而快。
5. 避坑与排查:复现路上最容易翻车的五件事
5.1 现象:cmake 配置 instant-ngp 直接失败,报语法不支持
原因基本只有一个:系统 cmake 版本低于 3.22,而 instant-ngp 的 CMakeLists 用了新版本才有的语法特性。这类报错通常不会明说"版本低",而是给一行莫名其妙的 parser error,误导性极强。
解决分两步。先cmake --version确认版本,低于 3.22 就按第 4.2 节装一个新版;装完必须hash -r,否则 shell 缓存里还是旧的可执行文件路径,cmake --version显示的还是老版本。多版本共存时可以用which cmake看实际命中的是哪一个,必要时在 PATH 里把/usr/local/bin提到前面。
5.2 现象:Could NOT find GLEW,或者链接阶段报一堆 GL 符号未定义
原因是系统没装 OpenGL 扩展加载库的开发包,或者装了但只有运行时库没有头文件。cmake 找 GLEW 依赖GLEW_INCLUDE_DIRS和GLEW_LIBRARIES两个变量,缺任何一个都会走到这个分支。
解决是sudo apt install libglew-dev,装完重新 cmake。如果仍报错,用dpkg -L libglew-dev看头文件确实落到/usr/include/GL/下没有,落到了别的地方就得手动传-DGLEW_INCLUDE_DIRS=...。同一类问题还会以 GLFW、X11、OpenGL 的面目出现,处理方式一致。
5.3 现象:Could NOT find Boost,缺一串 serialization/system/filesystem 组件
原因有三种可能,得逐个排。一是根本没装 Boost;二是装了但版本低于 1.65;三是装了新版但 cmake 优先找到了系统旧版的 CMake 配置文件,读出来的组件列表对不上。
解决是源码装 1.73 并sudo ldconfig。装完如果还报,就在 cmake 命令里显式加-DBOOST_ROOT=/usr/local -DBoost_NO_SYSTEM_PATHS=ON,把搜索路径锁死到新装的那一份。这一条我踩过两次,第二次是因为 apt 装过 libboost-all-dev,/usr/lib/x86_64-linux-gnu/cmake/Boost-1.71.0这个目录一直抢在前面,最后是把这个旧配置目录临时改名才干净解决。
5.4 现象:子模块目录是空的,cmake 报找不到 thirdparty/instant-ngp/CMakeLists.txt
原因是 clone 时没带--recurse-submodules,git 只拉了主仓库,两个子模块目录是空壳。这个错误信息很直白,但新手容易慌,以为是仓库结构本身有问题。
解决是git submodule update --init --recursive补拉。已经拉过一半的情况加--recursive也能力挽狂澜。判断标准很简单:ls thirdparty/instant-ngp和ls thirdparty/gtsam都应该有内容,空的就说明没拉全。
5.5 现象:gtsam 编译成功但 import 失败,训练脚本一起就 ImportError
原因大概率是 Python wrapper 编出来了但不在sys.path上,或者编译时链接的 Python 解释器和你当前 venv 里的不是同一个。后者更隐蔽:编 wrapper 时如果 venv 没激活,它会去链系统 Python 的头文件,编出来在 venv 里 import 就会报 ABI 不兼容。
解决是先echo $VIRTUAL_ENV确认在 venv 里,再find build_gtsam -name "gtsam*.so"找到编出来的模块,把所在目录加进PYTHONPATH后重试。如果是 ABI 问题,只能回退到"激活 venv 后重新 cmake + 重编"这一条路,没有捷径。
提示:这五条里,第 5.1 和 5.3 是最耗时的两个,建议在动手前先把 cmake 版本和 Boost 版本各查一遍,比事后 debug 省几个小时。
6. 二次开发:改哪几个点、怎么验证改动有效
跑通之后想往前推一步,改造点其实很集中,按投入产出比排一下大概是这样:
| 改造目标 | 动的位置 | 改动量 | 验证方式 |
|---|---|---|---|
| 换自己的数据集 | 数据加载与位姿初始化 | 小 | 前 50 帧位姿轨迹是否连续、无跳变 |
| 调深度监督权重 | λD 与协方差加权公式 | 极小 | 固定迭代次数看 PSNR 与深度 L1 |
| 换渲染后端 | Instant-NGP 的 hash 层配置 | 中 | 同等训练时长下的收敛曲线 |
| 接入外部位姿源 | 替换 Droid-SLAM 输出 | 中大 | 与真值轨迹做 ATE 对比 |
| 导出网格 | 从体密度场做 marching cubes | 中 | 可视化检查孔洞与噪点 |
最省事也最见效的是调 λD。具体做法是把 λD 做成命令行参数,在 0.3、1.0、2.0 三档各跑一次短序列,横轴训练迭代次数、纵轴深度 L1 和渲染 PSNR,画两条曲线。如果深度项权重压得太低,PSNR 会涨但几何会飘,表现为重建出来的墙面有波浪;权重太高,收敛会明显变慢,因为噪声像素在拖后腿。
# 把 λD 参数化,跑对比实验 parser.add_argument("--lambda_d", type=float, default=1.0, help="深度监督权重,噪声大时往下调,深度准时往上抬") # 训练循环里按迭代数做退火:前期重几何,后期重外观 lam = args.lambda_d * max(0.3, 1.0 - it / total_iters) loss = lam * depth_loss + color_loss上面这段退火是我自己加的,原文里 λD 是常数。加它的动机很简单:训练前期位姿和深度都还没收敛,几何约束能帮着把结构先立起来;到后期辐射场已经成形,深度项的噪声反而会干扰颜色细节,逐步降权更稳。这套写法不是官方给的,属于工程上的经验补丁,跑之前建议先做 A/B 对比,别直接上全长序列。
另一个值得动的地方是验证方法本身。我这里踩过最大的一个坑是只看渲染图好不好看,不看轨迹精度。渲染图有个特性——它特别能"骗人",辐射场可以把错误的几何渲染得很像样,但你把相机轨迹拉出来和真值一对比,ATE 能差出几十厘米。从那以后我每次改完代码,都强制走一遍"轨迹 ATE + 深度 L1 + 渲染 PSNR"三件套,三个数一起看才敢说改动有效,只要有一项变差就回滚。
如果要往工程化方向走,建议把跟踪和建图切成两个进程,用文件或者共享内存传位姿和深度图。这样跟踪崩了不用重建图,建图调参也不用重跑前端,调试效率会高很多。代价是多一层序列化和同步逻辑,帧率低的时候要考虑丢帧策略。这个改动不算小,但如果目标是长期迭代,早做比晚做好。希望帮到你。
本文还有配套的精品资源,点击获取