目标:读懂 MonoGS 从输入帧、相机跟踪、关键帧、局部建图到 3D Gaussian 增删和评测的完整链路;能跑通 TUM、Replica、EuRoC、Realsense 实操,能修改配置、定位问题,并能回答源码与系统设计面试题。
文章目录
- 先建立全局认识
- 项目定位与能力边界
- 本地源码快照与目录结构
- 环境、依赖与构建
- 运行入口与真实进程模型
- 总体数据流
- 配置系统精读
- [Dataset 与坐标系](#Dataset 与坐标系)
- [Camera 与位姿增量优化](#Camera 与位姿增量优化)
- [可微 Gaussian 渲染器](#可微 Gaussian 渲染器)
- GaussianModel:表示、初始化与优化器
- [Tracking 前端源码](#Tracking 前端源码)
- 关键帧与滑动窗口
- [Mapping 后端源码](#Mapping 后端源码)
- [Densification、Pruning 与可见性管理](#Densification、Pruning 与可见性管理)
- 损失函数与优化目标
- [多进程、队列与 GUI](#多进程、队列与 GUI)
- 评测、保存与输出文件
- [实操一:跑通官方 TUM 单目示例](#实操一:跑通官方 TUM 单目示例)
- [实操二:RGB-D、Replica 与单进程模式](#实操二:RGB-D、Replica 与单进程模式)
- [实操三:无 GUI 评测与结果检查](#实操三:无 GUI 评测与结果检查)
- [实操四:调试 Tracking、Mapping 和 Gaussian 数量](#实操四:调试 Tracking、Mapping 和 Gaussian 数量)
- 实操五:准备自定义数据集
- [实操六:实时 Realsense 与性能优化](#实操六:实时 Realsense 与性能优化)
- 推荐源码阅读路线
- 常见问题排查
- 面试常问问题
- 进阶练习
- 参考资料
1. 先建立全局认识
1.1 MonoGS 解决什么问题
MonoGS 将 SLAM 与 3D Gaussian Splatting 统一起来。它不先运行传统 SfM/COLMAP、再离线训练 3DGS;而是在输入图像序列时同时估计相机位姿、维护关键帧、增量构建 Gaussian 地图并渲染新视角。
新帧 -> Gaussian 渲染 -> RGB/Depth 误差 -> 相机位姿更新
-> 关键帧 -> 新高斯初始化 -> 局部 BA -> densify/prune
一句话:
MonoGS 用显式可微 Gaussian 地图同时承担"可渲染场景表示"和"直接法视觉跟踪"的角色。
1.2 运行时闭环
- 读取 RGB、RGB-D、Stereo 或 Realsense 帧。
- 用上一帧估计位姿初始化当前相机。
- 渲染当前相机视角。
- 反向传播 RGB/Depth photometric loss,更新当前相机 SE(3) 增量和曝光。
- 根据相对平移与高斯可见性重叠判断关键帧。
- 后端将关键帧 RGB 与真实/伪深度反投影为点云,加入 Gaussian map。
- 在关键帧滑动窗口内联合优化高斯、最近相机位姿和曝光。
- 用屏幕空间梯度 densify,用 opacity、尺寸和共视次数 prune。
1.3 与原版 3DGS 的区别
| 维度 | 原版 gaussian-splatting | MonoGS |
|---|---|---|
| 位姿来源 | 通常 COLMAP/SfM,训练中一般固定 | 前端跟踪,后端局部 BA 持续更新 |
| 场景初始化 | SfM 稀疏点云 | RGB-D/立体深度,或单目初始/渲染深度 |
| 优化方式 | 离线批量训练 | 增量式滑动窗口优化 |
| 相机梯度 | 主要优化 Gaussian | Rasterizer 支持 camera pose gradient |
| 目标 | 新视角渲染 | 同时定位、稠密建图和渲染 |
1.4 与传统 SLAM 的区别
| 系统 | 地图表示 | Tracking 残差 | 输出 |
|---|---|---|---|
| ORB-SLAM | 稀疏特征点 | 特征重投影误差 | 轨迹、稀疏地图 |
| Dense RGB-D SLAM | TSDF/Surfel/点云 | ICP/光度误差 | 稠密几何 |
| NeRF-SLAM | 隐式 MLP 场 | 体渲染 RGB/Depth 误差 | 隐式辐射场 |
| MonoGS | 显式 3D Gaussian | Gaussian 渲染 RGB/Depth 误差 | 轨迹、Gaussian PLY、渲染地图 |
2. 项目定位与能力边界
2.1 当前源码支持的输入
- 单目:configs/mono/tum。
- RGB-D TUM:configs/rgbd/tum。
- RGB-D Replica:configs/rgbd/replica。
- Stereo EuRoC:configs/stereo/euroc,README 标为 experimental。
- Realsense 实时输入:configs/live。
2.2 单目模式的重要事实
当前代码的单目路径包含研究复现用的初始化和评测先验:
- TUMParser 读取 groundtruth.txt 或 pose.txt。
- Camera 保存 R_gt/T_gt。
- FrontEnd.initialize() 调用 viewpoint.update_RT(viewpoint.R_gt, viewpoint.T_gt),初始帧直接采用参考位姿建立世界坐标规范。
- 后续帧的初值来自前一帧估计位姿,并非每帧使用 GT。
- 第一个单目关键帧没有真实深度,使用约 2.0 加噪声的初始深度。
- 后续关键帧主要使用当前 Gaussian 渲染深度的中值和标准差处理后作为伪深度。
因此当前单目实现不等于完整无先验部署系统。若要用于未知环境,还需要视觉初始化、尺度恢复、重定位和闭环等模块。
2.3 当前源码没有实现的完整能力
- 全局回环检测和 pose graph 优化。
- 失跟重定位。
- IMU/LiDAR 融合。
- 动态物体剔除。
- 大尺度分块、流式和 LOD。
- 多传感器时间同步。
2.4 可复现性
README 提示多进程 GPU 使用会带来少量随机性,并提供更快的 dev.speedup 分支。本地目录若没有 Git 元数据,无法从目录本身确认 commit;实验应记录源码来源、CUDA/PyTorch、GPU、YAML、数据集版本、GUI/single_thread 开关和评测命令。
3. 本地源码快照与目录结构
MonoGS/
├── slam.py # 主入口、进程装配、评测和保存
├── configs/ # mono / rgbd / stereo / live 配置
├── utils/
│ ├── slam_frontend.py # Tracking、关键帧、窗口管理
│ ├── slam_backend.py # 初始化、局部 BA、Gaussian 更新
│ ├── camera_utils.py # Camera 和可优化位姿/曝光
│ ├── pose_utils.py # SO(3)/SE(3) 指数映射
│ ├── slam_utils.py # Tracking/Mapping loss、深度统计
│ ├── dataset.py # TUM、Replica、EuRoC、Realsense
│ ├── eval_utils.py # ATE、PSNR、SSIM、LPIPS、保存
│ └── config_utils.py # YAML inherit_from
├── gaussian_splatting/
│ ├── scene/gaussian_model.py # Gaussian 参数、初始化、densify/prune
│ └── gaussian_renderer/ # Python 到 CUDA rasterizer 的接口
├── submodules/
│ ├── simple-knn/ # CUDA 近邻距离
│ └── diff-gaussian-rasterization/# 支持 pose gradient 的光栅化器
├── gui/ # OpenGL GUI 和 GaussianPacket
├── scripts/ # 数据集下载
└── environment.yml # 推荐 Conda 环境
3.1 阅读问题到文件的映射
| 问题 | 优先阅读 |
|---|---|
| 程序从哪里启动 | slam.py |
| 当前帧如何跟踪 | utils/slam_frontend.py 的 tracking |
| 何时成为关键帧 | is_keyframe、add_to_window |
| 地图如何初始化 | slam_backend.py 的 initialize_map |
| 局部 BA 如何优化 | slam_backend.py 的 map |
| 高斯参数是什么 | gaussian_model.py |
| 新高斯从哪里来 | create_pcd_from_image 系列 |
| 相机位姿如何更新 | camera_utils.py、pose_utils.py |
| 损失如何定义 | utils/slam_utils.py |
| 指标与输出 | utils/eval_utils.py |
4. 环境、依赖与构建
4.1 源码给出的版本组合
environment.yml 使用:
Python 3.7.13
PyTorch 1.12.1
Torchvision 0.13.1
CUDA Toolkit 11.6
Open3D 0.17.0
evo 1.11.0
并从本地 pip 安装:
submodules/simple-knn
submodules/diff-gaussian-rasterization
4.2 推荐环境
Ubuntu 20.04 或兼容 Linux
NVIDIA GPU、驱动和 CUDA/PyTorch ABI 匹配
Conda、gcc/g++、nvcc
源码依赖 CUDA 扩展、torch.multiprocessing 和 OpenGL GUI。Windows 适合阅读,完整运行更推荐 Ubuntu 或 WSL2 加 NVIDIA GPU。
4.3 安装与验证
bash
cd ~/project/MonoGS
conda env create -f environment.yml
conda activate MonoGS
python - <<'PY'
import torch
print("torch:", torch.__version__)
print("cuda available:", torch.cuda.is_available())
print("torch cuda:", torch.version.cuda)
print("gpu:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else None)
PY
如果是新克隆,必须递归拉取子模块:
bash
git clone --recursive https://github.com/muskie82/MonoGS.git
4.4 子模块验证
bash
python - <<'PY'
import simple_knn
import diff_gaussian_rasterization
print("CUDA extensions imported successfully")
PY
若 import 失败,在已激活环境中重新构建:
bash
pip install -v submodules/simple-knn
pip install -v submodules/diff-gaussian-rasterization
4.5 常见构建错误
| 现象 | 原因 | 处理 |
|---|---|---|
| nvcc 找不到 | toolkit/PATH 错误 | 检查 nvcc --version、CUDA_HOME |
| undefined symbol | 扩展与当前 PyTorch ABI 不一致 | 在当前环境重编译扩展 |
| unsupported gpu architecture | CUDA 版本过旧 | 换支持当前 GPU 的 CUDA/PyTorch 组合 |
| GUI 黑屏 | OpenGL/GLFW/display 问题 | 先关闭 GUI 验证核心算法 |
| CUDA 多进程错误 | fork/spawn 或 context 时机不对 | 保留源码 spawn 方式 |
5. 运行入口与真实进程模型
5.1 slam.py
运行:
bash
python slam.py --config configs/mono/tum/fr3_office.yaml
入口完成:解析参数、设置 mp.set_start_method("spawn")、递归加载 YAML、可选覆盖 eval 开关、创建结果目录/wandb、构造 SLAM。
5.2 关键细节:核心工作在构造函数
SLAM.run() 是空的:
python
def run(self):
pass
真实流程在 SLAM.init() 中完成:创建 GaussianModel/Dataset/队列,启动后端,调用 self.frontend.run(),完成后做评测、停止后端和 GUI。二次开发时不要把主线误判为从 slam.run() 开始。
5.3 真实进程关系
虽然 FrontEnd 继承 mp.Process,源码没有调用 frontend.start():
python
backend_process = mp.Process(target=self.backend.run)
backend_process.start()
self.frontend.run()
因此:
主进程:FrontEnd.run 加总体生命周期
子进程:BackEnd.run
可选子进程:GUI
5.4 初始化对象
SLAM.init() 创建:
GaussianModel
Dataset
FrontEnd / BackEnd
frontend_queue / backend_queue
GUI queues 或 FakeQueue
CUDA background = [0, 0, 0]
Training.spherical_harmonics 为真时,model_params.sh_degree 被设为 3;否则为 0。
6. 总体数据流
#mermaid-svg-cRvTr2LAXugMO43R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cRvTr2LAXugMO43R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cRvTr2LAXugMO43R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cRvTr2LAXugMO43R .error-icon{fill:#552222;}#mermaid-svg-cRvTr2LAXugMO43R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cRvTr2LAXugMO43R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cRvTr2LAXugMO43R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cRvTr2LAXugMO43R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cRvTr2LAXugMO43R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cRvTr2LAXugMO43R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cRvTr2LAXugMO43R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cRvTr2LAXugMO43R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cRvTr2LAXugMO43R .marker.cross{stroke:#333333;}#mermaid-svg-cRvTr2LAXugMO43R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cRvTr2LAXugMO43R p{margin:0;}#mermaid-svg-cRvTr2LAXugMO43R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cRvTr2LAXugMO43R .cluster-label text{fill:#333;}#mermaid-svg-cRvTr2LAXugMO43R .cluster-label span{color:#333;}#mermaid-svg-cRvTr2LAXugMO43R .cluster-label span p{background-color:transparent;}#mermaid-svg-cRvTr2LAXugMO43R .label text,#mermaid-svg-cRvTr2LAXugMO43R span{fill:#333;color:#333;}#mermaid-svg-cRvTr2LAXugMO43R .node rect,#mermaid-svg-cRvTr2LAXugMO43R .node circle,#mermaid-svg-cRvTr2LAXugMO43R .node ellipse,#mermaid-svg-cRvTr2LAXugMO43R .node polygon,#mermaid-svg-cRvTr2LAXugMO43R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cRvTr2LAXugMO43R .rough-node .label text,#mermaid-svg-cRvTr2LAXugMO43R .node .label text,#mermaid-svg-cRvTr2LAXugMO43R .image-shape .label,#mermaid-svg-cRvTr2LAXugMO43R .icon-shape .label{text-anchor:middle;}#mermaid-svg-cRvTr2LAXugMO43R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cRvTr2LAXugMO43R .rough-node .label,#mermaid-svg-cRvTr2LAXugMO43R .node .label,#mermaid-svg-cRvTr2LAXugMO43R .image-shape .label,#mermaid-svg-cRvTr2LAXugMO43R .icon-shape .label{text-align:center;}#mermaid-svg-cRvTr2LAXugMO43R .node.clickable{cursor:pointer;}#mermaid-svg-cRvTr2LAXugMO43R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cRvTr2LAXugMO43R .arrowheadPath{fill:#333333;}#mermaid-svg-cRvTr2LAXugMO43R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cRvTr2LAXugMO43R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cRvTr2LAXugMO43R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cRvTr2LAXugMO43R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cRvTr2LAXugMO43R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cRvTr2LAXugMO43R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cRvTr2LAXugMO43R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cRvTr2LAXugMO43R .cluster text{fill:#333;}#mermaid-svg-cRvTr2LAXugMO43R .cluster span{color:#333;}#mermaid-svg-cRvTr2LAXugMO43R div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cRvTr2LAXugMO43R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cRvTr2LAXugMO43R rect.text{fill:none;stroke-width:0;}#mermaid-svg-cRvTr2LAXugMO43R .icon-shape,#mermaid-svg-cRvTr2LAXugMO43R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cRvTr2LAXugMO43R .icon-shape p,#mermaid-svg-cRvTr2LAXugMO43R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cRvTr2LAXugMO43R .icon-shape .label rect,#mermaid-svg-cRvTr2LAXugMO43R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cRvTr2LAXugMO43R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cRvTr2LAXugMO43R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cRvTr2LAXugMO43R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} No
Init/Yes
Dataset: RGB / Depth / Pose
FrontEnd.run 主进程
Camera.init_from_dataset
Tracking: render + pose/exposure optimization
Keyframe?
清理当前非关键帧
backend_queue
BackEnd.run 子进程
初始化/扩展 Gaussian
Local mapping + Local BA
Densify / Prune
frontend_queue: sync_backend
GUI / trajectory / PLY / eval
6.1 队列协议
| 队列 | 方向 | 消息 |
|---|---|---|
| backend_queue | FrontEnd -> BackEnd | init、keyframe、pause、unpause、color_refinement、stop |
| frontend_queue | BackEnd -> FrontEnd | init、keyframe、sync_backend、stop |
前端发出的核心消息:
python
["init", frame_idx, viewpoint, depth_map]
["keyframe", frame_idx, viewpoint, current_window, depth_map]
后端同步消息:
python
[tag, clone_obj(gaussians), occ_aware_visibility, keyframe_poses]
6.2 为什么使用 clone
前后端处于不同进程。快照传递避免直接共享含梯度/优化器的 GPU 对象,但会带来序列化、拷贝和显存压力。实时优化时应把同步频率纳入性能分析。
7. 配置系统精读
7.1 YAML 继承
场景配置通过:
yaml
inherit_from: "configs/mono/tum/base_config.yaml"
继承基础参数。utils/config_utils.py 的 load_config() 递归加载父 YAML,再由 update_recursive() 用子项覆盖。
7.2 Results
yaml
Results:
save_results: True
use_gui: True
eval_rendering: False
use_wandb: False
- save_results:保存 config、轨迹、PLY 和评测结果。
- use_gui:启动 GUI;服务器优先关闭。
- eval_rendering:结束后评测渲染并进行颜色 refinement。
- use_wandb:记录实验。
7.3 Dataset
yaml
Dataset:
type: tum
sensor_type: monocular
pcd_downsample: 64
pcd_downsample_init: 32
adaptive_pointsize: True
point_size: 0.01
pcd_downsample 是随机点云下采样的倒数,数值越大新点越少;point_size 影响最近邻距离到初始 Gaussian scale 的转换。
7.4 Training 关键参数
| 参数 | 作用 |
|---|---|
| init_itr_num | 首帧地图初始化迭代数 |
| tracking_itr_num | 每普通帧位姿/曝光优化次数 |
| mapping_itr_num | 每关键帧映射迭代数 |
| window_size | 局部关键帧窗口大小 |
| pose_window | 后端可更新位姿的最近帧数 |
| kf_translation/kf_overlap | 关键帧平移与共视阈值 |
| gaussian_update_every/offset | densify/prune 节奏 |
| gaussian_th | opacity 裁剪阈值 |
| gaussian_reset | 非可见 Gaussian opacity reset 周期 |
| single_thread | 前后端是否串行协作 |
7.5 单进程配置
configs/rgbd/replica/office0.yaml 设置 Dataset.single_thread 为 false;对应 office0_sp.yaml 为 true。单进程更容易复现和调试,多进程吞吐更高但时序与 GPU 竞争更复杂。
8. Dataset 与坐标系
8.1 Loader
load_dataset() 按 Dataset.type 返回:
tum -> TUMDataset
replica -> ReplicaDataset
euroc -> EurocDataset
realsense -> RealsenseDataset
统一 getitem() 返回:
image: [3,H,W] float32 CUDA Tensor
depth: HxW numpy array 或 None
pose: 4x4 CUDA Tensor
8.2 TUM
TUMParser 读取 RGB、Depth、Pose 列表,按最近时间戳关联,最大间隔 0.08 秒,并按约 32 FPS 抽帧。TUM 四元数转 4x4 后求逆保存。
目录至少具备 rgb.txt、depth.txt、groundtruth.txt 或 pose.txt、rgb 图像与 depth 图像。
8.3 Replica 与 Stereo
Replica 读取 results/frame*.jpg、results/depth*.png 和 traj.txt。
EuRoC Stereo 使用 OpenCV StereoSGBM 算 disparity,源码用固定 47.90639384423901 / disparity 求深度。该数值等于特定标定下的 baseline 乘 fx,换相机必须重新计算。
8.4 位姿方向
Camera.R/T 被 getWorld2View2(R,T) 直接组装为:
T_C_W = [R | T]
即内部使用 world-to-camera。eval_ate() 对其取逆得到 camera-to-world 再比较 GT。自定义数据中最常见的错误是把 c2w 直接传给源码。
9. Camera 与位姿增量优化
9.1 Camera 状态
utils/camera_utils.py 的 Camera 保存:
R, T 当前估计 w2c
R_gt, T_gt Dataset 给出的参考 w2c
original_image, depth
fx, fy, cx, cy, FoVx, FoVy
cam_rot_delta, cam_trans_delta
exposure_a, exposure_b
9.2 SE(3) 更新
utils/pose_utils.py 的 update_pose():
tau = [cam_trans_delta, cam_rot_delta]
T_C_W_new = Exp_SE3(tau) * T_C_W_old
源码对 w2c 做左乘更新,写回 R/T 后将 delta 清零;若范数小于 1e-4 则认为本轮收敛。
9.3 曝光模型
loss 前使用:
I_adjusted = exp(exposure_a) * I_render + exposure_b
它能补偿整体亮度差,但不能解决滚动快门、动态物体、阴影和强镜面反射。
9.4 梯度 mask
compute_grad_mask() 用 Scharr 梯度生成高信息区域。Tracking RGB loss 采用有效 RGB mask 乘 gradient mask,以边缘和纹理像素提供更强的位姿约束;Mapping 则使用全部有效 RGB 像素。
10. 可微 Gaussian 渲染器
文件:gaussian_splatting/gaussian_renderer/init.py 的 render()。
10.1 输出
python
{
"render": rendered_image,
"viewspace_points": screenspace_points,
"visibility_filter": radii > 0,
"radii": radii,
"depth": depth,
"opacity": opacity,
"n_touched": n_touched,
}
10.2 屏幕空间梯度
Renderer 创建可求导的 screenspace_points 并 retain_grad。后端将其前两维梯度范数累积到 xyz_gradient_accum,作为 clone/split 的依据。
10.3 Covariance 与 SH
scale = exp(_scaling)
rotation = normalize(quaternion)
Sigma = L * L^T
关闭 spherical_harmonics 时使用 SH degree 0,只保留视角无关颜色;开启时使用 degree 3。
10.4 Pose gradient
子模块来自 README 指定的 diff-gaussian-rasterization-w-pose。Renderer 额外传入:
python
theta=viewpoint_camera.cam_rot_delta
rho=viewpoint_camera.cam_trans_delta
因此 RGB/Depth loss 可以回传到相机位姿增量,这是 MonoGS 直接法 Tracking 与 Local BA 的关键。
10.5 可见性
- radii 大于 0:未被裁剪的可见 Gaussian。
- n_touched:当前视角对每个 Gaussian 的触达/贡献信息。
- opacity:像素级渲染不透明度,RGB-D Tracking 中用于筛选可信深度。
11. GaussianModel:表示、初始化与优化器
11.1 参数张量
文件:gaussian_splatting/scene/gaussian_model.py。
_xyz N x 3
_features_dc N x 1 x 3
_features_rest N x (K-1) x 3
_scaling N x 3 或 N x 1,log-scale
_rotation N x 4,quaternion
_opacity N x 1,logit
附加 SLAM 元数据:
unique_kfIDs Gaussian 首次加入时所属关键帧
n_obs 当前窗口中被观测到的次数
max_radii2D 历史最大屏幕半径
xyz_gradient_accum / denom
11.2 从关键帧创建点云
create_pcd_from_image():
- 使用当前曝光参数修正 RGB。
- 选择外部 depthmap、真实深度或单目伪深度。
- 用 Open3D RGBDImage 和 PinholeCameraIntrinsic 反投影。
- 以当前 w2c 将点转换到世界相关坐标。
- 随机下采样并生成 BasicPointCloud。
11.3 初始化 Gaussian 参数
xyz <- 反投影点云位置
f_dc <- RGB2SH(color)
f_rest <- 0
scale <- log(sqrt(nearest_neighbor_distance * point_size))
rotation <- identity quaternion [1, 0, 0, 0]
opacity <- inverse_sigmoid(0.5)
最近邻平方距离由 CUDA simple_knn.distCUDA2 计算。
11.4 Optimizer
training_setup() 为 xyz、f_dc、f_rest、opacity、scaling、rotation 建立 Adam 参数组。位置学习率使用指数调度;其余参数按 YAML 设置。
11.5 PLY 保存
save_ply() 导出 x/y/z、f_dc、f_rest、opacity、scale、rot 等属性。这是训练后的 Gaussian PLY,不是普通 xyz+rgb 点云。
12. Tracking 前端源码
12.1 主循环
文件:utils/slam_frontend.py 的 FrontEnd.run()。
Dataset[idx]
-> Camera.init_from_dataset
-> compute_grad_mask
-> initialize 或 tracking
-> keyframe decision
-> keyframe 或 cleanup
-> 消费 backend 同步消息
12.2 初始化
首次 reset 时:
python
viewpoint.update_RT(viewpoint.R_gt, viewpoint.T_gt)
depth_map = self.add_new_keyframe(cur_frame_idx, init=True)
self.request_init(cur_frame_idx, viewpoint, depth_map)
后端收到 init 后清空旧地图、从首关键帧创建 Gaussian,并运行 init_itr_num 次初始化优化。
12.3 当前帧位姿初值
python
prev = self.cameras[cur_frame_idx - self.use_every_n_frames]
viewpoint.update_RT(prev.R, prev.T)
这是 constant pose 初始化。高速运动、低帧率或失跟后可能不够,需要 IMU、运动模型或重定位增强。
12.4 Tracking 优化变量
每帧临时创建 Adam,优化:
cam_rot_delta
cam_trans_delta
exposure_a
exposure_b
循环为:
render
-> get_loss_tracking
-> backward
-> optimizer.step
-> update_pose
12.5 Tracking loss
单目:
L_track_rgb = mean(opacity * |I_render * M - I_gt * M|)
M = valid_rgb * grad_mask
RGB-D:
L_track = alpha * L_rgb + (1-alpha) * L_depth
深度项仅在 gt_depth 大于 0.01 且 opacity 大于 0.95 的像素生效。
12.6 清理非关键帧
cleanup() 对普通帧调用 Camera.clean(),释放图像、深度、梯度 mask、位姿 delta 和曝光参数;每十帧调用 torch.cuda.empty_cache()。
13. 关键帧与滑动窗口
13.1 关键帧判据
is_keyframe() 综合:
相对平移距离
当前帧与最近关键帧的 Gaussian 可见性重叠
当前中值深度
平移条件近似为:
dist > kf_translation * median_depth
或
dist > kf_min_translation * median_depth 且 overlap 不足
可见性重叠计算的是 current 与 last keyframe 的 visible Gaussian 交并比。
13.2 滑动窗口
add_to_window() 把当前关键帧放在最前:
window = [current_kf, newest_old_kf, ..., oldest_kf]
因此 pose_window 表示最近若干关键帧,而非窗口中任意若干帧。
13.3 剔除策略
- 前两个帧不轻易删除。
- 优先删除与当前帧共视很低的帧。
- 窗口仍超限时,删除信息冗余较大的帧。
13.4 单目 reset
若单目系统尚未 initialized,窗口又移除了关键帧,前端会 reset 并输出关键帧重叠不足。开始阶段只原地旋转、纹理很弱或运动过快都容易触发。
14. Mapping 后端源码
14.1 职责
BackEnd 负责:
- 初始化/重置 Gaussian map。
- 新关键帧反投影成点云并扩展 Gaussian。
- 局部窗口重渲染和 mapping loss。
- 局部相机位姿/曝光 BA。
- Densify、Prune、opacity reset。
- 向前端同步快照和关键帧位姿。
- 可选结束后的颜色 refinement。
14.2 初始化地图
收到 init 后:
reset
-> viewpoints[first] = viewpoint
-> add_next_kf(init=True)
-> initialize_map
initialize_map 对首视角迭代 init_itr_num 次:
render -> mapping loss -> backward
-> radii/gradient stats
-> 周期 densify_and_prune
-> 指定迭代 opacity reset
-> Gaussian optimizer step
14.3 新关键帧
python
self.gaussians.extend_from_pcd_seq(
viewpoint, kf_id=frame_idx, init=init, depthmap=depth_map
)
即新图像加深度反投影成点云,再拼接为可学习 Gaussian 参数。
14.4 map() 的主要流程
- 取 current_window 关键帧作为主优化视角。
- 收集窗口外旧关键帧。
- 渲染窗口内所有关键帧并累计 mapping loss。
- 随机额外采样最多两个窗口外视角,缓解遗忘。
- 加 scale 各向同性正则。
- 反向传播。
- 更新可见性、densification 统计和 Gaussian optimizer。
- 更新最近 pose_window 个关键帧位姿和窗口帧曝光。
14.5 Local BA
后端的 keyframe_optimizers 对:
- 除首帧外最近 pose_window 个帧优化 cam_rot_delta/cam_trans_delta。
- 所有窗口帧优化 exposure_a/exposure_b。
因此局部联合优化是:
Gaussian parameters + recent keyframe poses + exposure
14.6 color_refinement
eval rendering 启用后,主进程会发送 color_refinement。后端固定随机关键帧,使用:
(1 - lambda_dssim) * L1 + lambda_dssim * (1 - SSIM)
进行 26000 次颜色精修,随后同步最终 Gaussian。普通在线 Mapping 不是这套目标。
15. Densification、Pruning 与可见性管理
15.1 Clone 与 Split
densify_and_prune() 先计算:
grads = xyz_gradient_accum / denom
- densify_and_clone:高梯度且尺寸较小的 Gaussian 直接复制。
- densify_and_split:高梯度且尺寸较大的 Gaussian 按局部高斯分布采样多个子点、缩小 scale,再删除父点。
15.2 常规裁剪
根据以下条件裁剪:
- opacity 太低。
- 屏幕半径过大。
- 世界尺度过大。
15.3 SLAM 共视裁剪
窗口满时,后端统计每个 Gaussian 在当前窗口中的 n_obs。prune_mode 为 slam 时,主要删除较新区域、且 n_obs 小于等于 3 的 Gaussian。它避免只被极少数帧看到的近期点长期污染地图。
15.4 Opacity reset
每隔 gaussian_reset 次 Mapping,且当前不是 densify 轮时调用 reset_opacity_nonvisible。目标是对当前不可见的点降低/重置 opacity,促进重新优化和后续清理。
15.5 数量爆炸的调参顺序
- 先检查位姿、深度和内参。
- 增大 pcd_downsample,降低新增点密度。
- 提高 densify_grad_threshold。
- 提高 gaussian_th 或增强 prune。
- 降低窗口或 mapping 次数做性能实验。
- 确认不是动态物体或错误深度触发持续高梯度。
16. 损失函数与优化目标
文件:utils/slam_utils.py。
16.1 Tracking
单目:
L_track = mean(opacity * |I_render * M - I_gt * M|)
M = valid_rgb * grad_mask
RGB-D:
L_track = alpha * L_rgb + (1-alpha) * L_depth
16.2 Mapping
单目 Mapping 是全部有效 RGB 像素的 L1。RGB-D Mapping 同样使用 RGB/Depth 加权 L1。
后端还加:
L_iso = 10 * mean(|scale - mean(scale)|)
它抑制同一 Gaussian 三轴尺度过度差异,减少由位姿和深度误差造成的极端扁长 Gaussian。
16.3 损失诊断
| 现象 | 优先检查 |
|---|---|
| Tracking loss 不降 | 位姿初值、内参、梯度 mask、pose gradient 扩展 |
| 深度 loss 很大 | depth_scale、深度对齐、单位、c2w/w2c |
| 单目频繁 reset | 初始化视差、重叠、伪深度、运动速度 |
| 高斯拉成长条 | 位姿误差、深度错、scale 正则不足 |
| 颜色重影 | 曝光、动态物体、关键帧窗口、光度模型 |
17. 多进程、队列与 GUI
#mermaid-svg-g584HyLbxRMoKj7Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-g584HyLbxRMoKj7Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-g584HyLbxRMoKj7Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-g584HyLbxRMoKj7Y .error-icon{fill:#552222;}#mermaid-svg-g584HyLbxRMoKj7Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-g584HyLbxRMoKj7Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-g584HyLbxRMoKj7Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-g584HyLbxRMoKj7Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-g584HyLbxRMoKj7Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-g584HyLbxRMoKj7Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-g584HyLbxRMoKj7Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-g584HyLbxRMoKj7Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-g584HyLbxRMoKj7Y .marker.cross{stroke:#333333;}#mermaid-svg-g584HyLbxRMoKj7Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-g584HyLbxRMoKj7Y p{margin:0;}#mermaid-svg-g584HyLbxRMoKj7Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-g584HyLbxRMoKj7Y .cluster-label text{fill:#333;}#mermaid-svg-g584HyLbxRMoKj7Y .cluster-label span{color:#333;}#mermaid-svg-g584HyLbxRMoKj7Y .cluster-label span p{background-color:transparent;}#mermaid-svg-g584HyLbxRMoKj7Y .label text,#mermaid-svg-g584HyLbxRMoKj7Y span{fill:#333;color:#333;}#mermaid-svg-g584HyLbxRMoKj7Y .node rect,#mermaid-svg-g584HyLbxRMoKj7Y .node circle,#mermaid-svg-g584HyLbxRMoKj7Y .node ellipse,#mermaid-svg-g584HyLbxRMoKj7Y .node polygon,#mermaid-svg-g584HyLbxRMoKj7Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-g584HyLbxRMoKj7Y .rough-node .label text,#mermaid-svg-g584HyLbxRMoKj7Y .node .label text,#mermaid-svg-g584HyLbxRMoKj7Y .image-shape .label,#mermaid-svg-g584HyLbxRMoKj7Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-g584HyLbxRMoKj7Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-g584HyLbxRMoKj7Y .rough-node .label,#mermaid-svg-g584HyLbxRMoKj7Y .node .label,#mermaid-svg-g584HyLbxRMoKj7Y .image-shape .label,#mermaid-svg-g584HyLbxRMoKj7Y .icon-shape .label{text-align:center;}#mermaid-svg-g584HyLbxRMoKj7Y .node.clickable{cursor:pointer;}#mermaid-svg-g584HyLbxRMoKj7Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-g584HyLbxRMoKj7Y .arrowheadPath{fill:#333333;}#mermaid-svg-g584HyLbxRMoKj7Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-g584HyLbxRMoKj7Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-g584HyLbxRMoKj7Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g584HyLbxRMoKj7Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-g584HyLbxRMoKj7Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g584HyLbxRMoKj7Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-g584HyLbxRMoKj7Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-g584HyLbxRMoKj7Y .cluster text{fill:#333;}#mermaid-svg-g584HyLbxRMoKj7Y .cluster span{color:#333;}#mermaid-svg-g584HyLbxRMoKj7Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-g584HyLbxRMoKj7Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-g584HyLbxRMoKj7Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-g584HyLbxRMoKj7Y .icon-shape,#mermaid-svg-g584HyLbxRMoKj7Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g584HyLbxRMoKj7Y .icon-shape p,#mermaid-svg-g584HyLbxRMoKj7Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-g584HyLbxRMoKj7Y .icon-shape .label rect,#mermaid-svg-g584HyLbxRMoKj7Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g584HyLbxRMoKj7Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-g584HyLbxRMoKj7Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-g584HyLbxRMoKj7Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} backend_queue
frontend_queue
q_main2vis
q_vis2main
Main process: FrontEnd.run
BackEnd process
GUI process
17.1 后端空闲时做什么
BackEnd.run 在没有新消息且窗口非空时持续 map(current_window)。last_sent 达到 10 后会额外 map(..., prune=True, iters=10),再 push_to_frontend。
17.2 single_thread
single_thread 为真时,前端在已有待处理关键帧时暂停读取新帧,等待后端完成。它利于调试和复现,代价是吞吐下降。
17.3 GUI Packet
gui_utils.GaussianPacket 拷贝 xyz、opacity、scaling、rotation、SH features、keyframe IDs、n_obs、当前帧和关键帧。GUI 应只是观测层,不应成为算法正确性的依赖;服务器先用 use_gui false 验证。
17.4 多进程风险
- 两个进程争抢同一 GPU。
- Gaussian 快照传输有拷贝成本。
- Queue.empty 在并发语义上不是严格可靠的状态判断,源码作为轻量轮询使用。
- CUDA context、spawn 和扩展导入时机错误会启动失败。
- GUI 异常可能影响主流程。
18. 评测、保存与输出文件
18.1 eval 命令
bash
python slam.py --config configs/mono/tum/fr3_office.yaml --eval
该命令会自动:
save_results = True
use_gui = False
eval_rendering = True
use_wandb = True
18.2 ATE
eval_ate() 从关键帧取估计/GT w2c,取逆得到 c2w,调用 evo 对齐并计算平移 APE RMSE。单目模式使用 correct_scale=true,所以单目 ATE 不能单独证明绝对尺度正确。
18.3 渲染指标
eval_rendering() 每隔 5 帧取一个非关键帧,计算:
- PSNR:越高越好。
- SSIM:越高越好。
- LPIPS:越低越好。
18.4 输出目录
save_results 开启后,常见结果:
<save_dir>/<dataset tag>/<timestamp>/
├── config.yml
├── plot/trj_final.json
├── plot/evo_2dplot_final.png
├── psnr/before_opt/final_result.json
├── psnr/after_opt/final_result.json
├── point_cloud/final/point_cloud.ply
└── point_cloud/final_after_opt/point_cloud.ply
18.5 PLY 检查脚本
python
from plyfile import PlyData
ply = PlyData.read("results/.../point_cloud/final/point_cloud.ply")
vertex = ply["vertex"]
print("gaussian count:", vertex.count)
print("properties:", vertex.data.dtype.names)
19. 实操一:跑通官方 TUM 单目示例
19.1 下载数据
bash
cd ~/project/MonoGS
bash scripts/download_tum.sh
python slam.py --config configs/mono/tum/fr3_office.yaml
19.2 首次运行建议关闭 GUI
复制一份配置并覆盖:
yaml
inherit_from: "configs/mono/tum/fr3_office.yaml"
Results:
use_gui: false
use_wandb: false
save_results: true
运行:
bash
python slam.py --config configs/mono/tum/fr3_office_headless.yaml
先确认 headless 成功,再处理 OpenGL。
19.3 初次成功标准
- 没有 CUDA extension/import 错误。
- 没有持续 reset。
- 至少产生多个 keyframe。
- 输出 final point_cloud.ply。
- 轨迹图连续。
- 渲染没有整体镜像或彻底发散。
19.4 单目启动动作建议
- 初始阶段缓慢平移,提供视差。
- 保持纹理丰富、静态环境。
- 避免只原地旋转。
- 避免白墙、镜面和强曝光变化。
- 初始 BA 前不要快速挥动相机。
20. 实操二:RGB-D、Replica 与单进程模式
20.1 TUM RGB-D
bash
python slam.py --config configs/rgbd/tum/fr3_office.yaml
RGB-D 配置使用 sensor_type 为 depth,真实深度会参与初始化、Tracking 和 Mapping,通常比单目稳定。
20.2 Replica
bash
bash scripts/download_replica.sh
python slam.py --config configs/rgbd/replica/office0.yaml
Replica 的分辨率和 depth_scale 与 TUM 不同。不要把 TUM 的 depth_scale 5000 复制给 Replica,源码配置使用 6553.5。
20.3 单进程
bash
python slam.py --config configs/rgbd/replica/office0_sp.yaml
该配置将 Dataset.single_thread 设为 true,适合复现、阅读日志和排查同步问题,不适合追求实时帧率。
20.4 推荐消融
| 实验 | 输入 | 重点 |
|---|---|---|
| A | 单目、多进程 | 初始化成功率、ATE、Gaussian 数量 |
| B | 单目、single_thread | 同步对收敛的影响 |
| C | RGB-D、多进程 | 深度对轨迹和几何的改善 |
| D | RGB-D、single_thread | 可重复性和性能代价 |
21. 实操三:无 GUI 评测与结果检查
21.1 标准评测
bash
python slam.py --config configs/mono/tum/fr3_office.yaml --eval
评测不只是输出数字:它先做 before_opt 渲染评测,再触发后端 26000 次 color_refinement,最后做 after_opt 评测。因此评测 FPS 不能直接与普通在线运行 FPS 比较。
21.2 查看轨迹 JSON
python
import json
import numpy as np
with open("results/.../plot/trj_final.json", encoding="utf-8") as file:
data = json.load(file)
poses = np.asarray(data["trj_est"])
print("estimated poses:", poses.shape)
print("first c2w pose:\n", poses[0])
21.3 指标解读
- ATE:轨迹误差,单目会尺度对齐。
- PSNR:像素误差指标。
- SSIM:结构相似度。
- LPIPS:感知差异。
- FPS:源码按前端帧数除总时长计算,受 GUI、后端、评测开关影响。
22. 实操四:调试 Tracking、Mapping 和 Gaussian 数量
22.1 调试顺序
数据路径/时间顺序
-> 图像尺寸/内参/畸变
-> pose 方向
-> render 是否合理
-> tracking loss
-> keyframe/window
-> mapping/densify/prune
22.2 Tracking 诊断
可在 FrontEnd.tracking 的循环中临时加入:
python
if tracking_itr % 10 == 0:
print(
"frame=", cur_frame_idx,
"iter=", tracking_itr,
"loss=", float(loss_tracking.detach()),
"median_depth=", float(depth.median().detach()),
"mean_opacity=", float(opacity.mean().detach()),
)
观察 loss 是否下降、深度是否为 0/NaN/极大、opacity 是否接近 0、位姿增量是否异常。
22.3 Mapping 诊断
python
if self.iteration_count % 20 == 0:
print(
"map_iter=", self.iteration_count,
"loss=", float(loss_mapping.detach()),
"gaussians=", self.gaussians.get_xyz.shape[0],
"window=", current_window,
)
22.4 可见性诊断
python
visible = render_pkg["visibility_filter"].sum().item()
touched = (render_pkg["n_touched"] > 0).sum().item()
total = self.gaussians.get_xyz.shape[0]
print(f"visible={visible}/{total}, touched={touched}/{total}")
若可见高斯接近 0,优先查 w2c/c2w、内参、图像尺寸、首帧深度和相机朝向。
22.5 Gaussian 数量
记录每个关键帧后的高斯数量。持续指数增长通常先怀疑位姿/深度错误或 densify 过激;几乎不增长则检查新增点、梯度阈值和关键帧数量;周期大幅下降则检查 prune。
23. 实操五:准备自定义数据集
23.1 最小 Dataset 契约
自定义类应提供:
num_imgs
fx, fy, cx, cy
width, height
fovx, fovy
__getitem__(idx) -> image, depth, pose
约定:
image: [3,H,W] float32, [0,1], cuda
depth: HxW numpy,单位米,未知值为 0;单目可为 None
pose: 4x4 torch,源码期望 world-to-camera
23.2 RGB-D Dataset 骨架
python
import numpy as np
import torch
from PIL import Image
from gaussian_splatting.utils.graphics_utils import focal2fov
from utils.dataset import BaseDataset
class CustomRGBDDataset(BaseDataset):
def __init__(self, args, path, config):
super().__init__(args, path, config)
calib = config["Dataset"]["Calibration"]
self.fx = calib["fx"]
self.fy = calib["fy"]
self.cx = calib["cx"]
self.cy = calib["cy"]
self.width = calib["width"]
self.height = calib["height"]
self.fovx = focal2fov(self.fx, self.width)
self.fovy = focal2fov(self.fy, self.height)
self.depth_scale = calib["depth_scale"]
self.color_paths = []
self.depth_paths = []
self.poses_w2c = []
self.num_imgs = len(self.color_paths)
def __getitem__(self, idx):
rgb = np.asarray(Image.open(self.color_paths[idx]).convert("RGB"))
depth = np.asarray(Image.open(self.depth_paths[idx]), dtype=np.float32)
depth = depth / self.depth_scale
depth[~np.isfinite(depth)] = 0
depth[depth < 0] = 0
image = (
torch.from_numpy(rgb / 255.0)
.permute(2, 0, 1)
.contiguous()
.to(device=self.device, dtype=self.dtype)
)
pose = torch.from_numpy(self.poses_w2c[idx]).to(
device=self.device, dtype=self.dtype
)
return image, depth, pose
23.3 注册与 YAML
在 load_dataset() 加入 custom_rgbd 分支,并新增 YAML:
yaml
inherit_from: "configs/rgbd/tum/base_config.yaml"
Dataset:
type: custom_rgbd
dataset_path: "/absolute/path/to/data"
sensor_type: depth
Calibration:
fx: 600.0
fy: 600.0
cx: 640.0
cy: 360.0
width: 1280
height: 720
depth_scale: 1000.0
distorted: false
k1: 0.0
k2: 0.0
p1: 0.0
p2: 0.0
k3: 0.0
23.4 五个高频错误
- 传入 c2w,但源码要 w2c。
- 深度毫米/原始 16-bit 没有除 depth_scale。
- OpenCV BGR 当成 RGB。
- resize/crop 后内参没更新。
- 畸变图像却使用未畸变 pinhole 内参。
23.5 自定义单目的现实问题
当前单目路径仍要求 Dataset 返回 pose,且首帧使用 R_gt/T_gt。没有 GT 时,必须额外实现视觉初始化、初始尺度、初始深度/点云、重定位和无 GT 评测,不能只写一个 Dataset loader。
24. 实操六:实时 Realsense 与性能优化
24.1 Realsense
bash
conda activate MonoGS
pip install pyrealsense2
python slam.py --config configs/live/realsense.yaml
RGB-D 使用 realsense_rgbd.yaml。连接 USB 3 端口,并先确认系统可识别设备。
24.2 运行建议
- 使用全局快门或低 rolling-shutter 相机。
- 固定曝光、白平衡,避免帧间颜色跳变。
- 初始阶段缓慢平移,提供足够视差。
- 静态环境优先。
- 源码 live_mode 会强制 use_gui 为 true;做纯性能实验时应修改并明确记录。
24.3 性能瓶颈
| 模块 | 主要开销 |
|---|---|
| Rasterizer | 投影、排序、alpha blending、反向传播 |
| Tracking | 每帧 tracking_itr_num 次 render/backward |
| Mapping | 多相机 render/backward 与局部 BA |
| Densification | 新点创建和 optimizer 参数拼接 |
| Queue | Gaussian 快照拷贝与进程通信 |
| GUI | OpenGL 渲染和数据搬运 |
24.4 第一轮性能调参
yaml
Results:
use_gui: false
eval_rendering: false
Training:
tracking_itr_num: 50
mapping_itr_num: 80
window_size: 6
pose_window: 3
先让位姿稳定,再逐步调速度。不要在 Tracking 尚未可靠时只追求 FPS。
24.5 GPU 监控
bash
nvidia-smi dmon -s pucvmet
记录 GPU util、显存、功耗、CPU 使用率和两个进程的 GPU 竞争情况。
25. 推荐源码阅读路线
第 1 轮:全局
目标:知道输入、配置、进程和输出位置。
第 2 轮:Tracking
- utils/camera_utils.py。
- utils/pose_utils.py。
- utils/slam_utils.py 的 get_loss_tracking。
- utils/slam_frontend.py 的 tracking 与 run。
目标:说明相机位姿为什么能从渲染损失中更新。
第 3 轮:高斯和渲染
- gaussian_model.py。
- gaussian_renderer/init.py。
- graphics_utils.py。
- diff-gaussian-rasterization 子模块。
目标:掌握 Gaussian 参数、投影、SH、opacity、2D gradient 和 pose gradient。
第 4 轮:后端
- BackEnd.initialize_map。
- BackEnd.map。
- BackEnd.run。
- GaussianModel.densify_and_prune。
目标:能够画出局部 BA、可见性和高斯增删顺序。
第 5 轮:工程
- eval_utils.py。
- gui_utils.py 与 slam_gui.py。
- multiprocessing_utils.py。
- environment.yml、Dependencies.md。
目标:理解指标、输出、性能和可复现性。
26. 常见问题排查
26.1 找不到 CUDA 扩展
bash
which python
python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())"
which nvcc
nvcc --version
确认子模块存在且在当前 Conda 环境安装。PyTorch/CUDA 变化后通常需要重新编译两个 CUDA 子模块。
26.2 GUI 无法启动
先在 YAML 关闭 GUI。若 headless 能跑,问题通常在 OpenGL、X11/Wayland、远程桌面或 GLFW,不在 SLAM 核心。
26.3 首帧后不断 reset
常见原因:
- 初始运动太快或只有旋转。
- 场景纹理不足。
- 单目伪深度不稳定。
- kf_translation/kf_overlap/window_size 不适合数据。
- 内参或图像尺寸不一致。
先用 RGB-D 验证完整管线,再回到单目。
26.4 Tracking 发散
优先查:
- fx/fy/cx/cy/width/height。
- c2w/w2c。
- RGB/BGR。
- 畸变与去畸变。
- pose gradient 子模块。
- 单目初始深度和场景尺度。
26.5 深度尺度错误
打印有效深度统计:
python
print(depth[depth > 0].min(), np.median(depth[depth > 0]), depth.max())
室内深度中值通常应是合理米级值,而不是 2000 或 0.002。
26.6 显存不断增长
检查 Gaussian 数量、关键帧数、GUI 快照频率、LPIPS 评测和是否意外保留 computation graph。先用曲线定位是 densify 还是对象生命周期问题。
26.7 地图很糊、很厚或浮点很多
优先检查位姿、深度、关键帧基线和动态物体。错误位姿会持续制造梯度,单纯增加 Gaussian 数量通常让问题更严重。
26.8 多进程不稳定
改用 _sp.yaml,关闭 GUI/wandb,固定数据和配置,多次运行报告均值和方差。README 已提示多进程 GPU 时序会带来随机性。
26.9 eval 很慢
eval 还会执行 26000 次 color refinement 和非关键帧 LPIPS,不只是输出指标。性能测试与画质评测应分开。
27. 面试常问问题
Q1:MonoGS 是什么?
MonoGS 是将 3D Gaussian Splatting 作为稠密地图表示的 SLAM 系统,通过可微 Gaussian 渲染同时完成相机跟踪、局部建图和新视角渲染。
Q2:它和原版 3DGS 最大区别是什么?
原版通常依赖 COLMAP 位姿和稀疏点云离线训练;MonoGS 将相机位姿作为可优化变量,前端实时 Tracking,后端在滑动窗口内联合优化高斯和相机。
Q3:Tracking 和 Mapping 如何分工?
Tracking 前端对每帧优化当前相机位姿和曝光;Mapping 后端只处理关键帧,扩展 Gaussian、做局部 BA、densify/prune,再同步结果。
Q4:当前源码真实的进程模型?
主进程直接运行 FrontEnd.run,独立 BackEnd 子进程执行 map,启用 GUI 时额外启动 GUI 子进程。前后端通过两个 mp.Queue 交换消息。
Q5:为什么 SLAM.run 是空的?
当前实现把核心运行生命周期写在 SLAM.init 中。run 只是未使用占位,读源码应从 main 和构造函数追踪。
Q6:Camera 的 R/T 是 c2w 还是 w2c?
源码直接用 R/T 构造 T_C_W,内部是 world-to-camera;ATE 评测时对其取逆得到 c2w。
Q7:如何从 RGB loss 优化相机位姿?
自定义 diff Gaussian rasterizer 接收 cam_rot_delta/cam_trans_delta,photometric loss 反向传播得到 pose gradient;update_pose 用 SE(3) Exp 左乘更新 w2c。
Q8:为什么用 SE(3) 增量?
保证旋转仍在 SO(3) 流形上,只有 6 自由度,局部优化更稳定,避免直接优化旋转矩阵破坏正交性。
Q9:Tracking 为什么使用梯度 mask?
平坦区域对位姿变化不敏感。源码用 Scharr 选择高梯度像素,提供更强直接法约束并减少低信息区域影响。
Q10:曝光参数有什么作用?
使用 exp(a) 乘渲染颜色再加 b,补偿帧间整体亮度差;它不能代替完整光照、去模糊或动态物体模型。
Q11:一个 MonoGS Gaussian 有哪些参数?
xyz、SH 颜色、opacity、scale、rotation;此外有关键帧 ID、观测次数、最大屏幕半径和 densification 梯度统计。
Q12:新 Gaussian 如何初始化?
将关键帧 RGB 与深度反投影为点云;位置初始化 xyz,RGB 转 SH DC,近邻距离初始化 scale,rotation 为单位四元数,opacity 初始为 0.5。
Q13:单目没有深度时怎么办?
首帧使用常数加噪声深度,后续用当前渲染深度的中值、标准差和 opacity 生成伪深度。这是初始化手段,几何尺度仍有局限。
Q14:为什么单目源码还读取 GT pose?
数据加载器提供评测/初始化参考,首帧直接以 GT pose 建立世界坐标。后续跟踪不是每帧使用 GT,但这说明它不是完整无先验部署链路。
Q15:densify 的依据是什么?
累计可见 Gaussian 屏幕空间投影点的 2D 梯度。高梯度且小的点 clone,高梯度且大的点 split。
Q16:MonoGS 如何 prune?
按低 opacity、过大尺寸裁剪,并在 SLAM 模式下根据当前窗口共视次数 n_obs 删除较新且很少被看到的 Gaussian。
Q17:为什么使用滑动窗口?
优化所有历史帧会线性增长且无法在线。窗口保留局部几何约束,再随机采样少数历史视角减轻遗忘。
Q18:pose_window 与 window_size 的区别?
window_size 是局部窗口总关键帧数;pose_window 是最近、允许继续优化位姿的关键帧数。
Q19:Mapping 为什么加 isotropic loss?
源码惩罚三个 scale 分量差异,抑制错误位姿/深度造成的极端扁长 Gaussian,提高局部优化稳定性。
Q20:RGB-D loss 怎样写?
alpha 乘 RGB L1 加 1-alpha 乘 Depth L1。Tracking 深度项只在 GT 深度有效且渲染 opacity 高的区域计算。
Q21:color refinement 和普通 Mapping 的差异?
普通 Mapping 做局部几何和颜色联合优化;结束后 refinement 随机选关键帧,用 L1 加 SSIM 做固定 26000 次外观精修。
Q22:多进程为什么有随机性?
GPU 竞争、消息队列时序、随机历史视角采样和 CUDA 非确定性都会改变优化轨迹,README 也明确提示这一点。
Q23:single_thread 的价值?
前端等待后端处理关键帧,减少异步差异,便于调试和复现;代价是吞吐与实时性下降。
Q24:如何评测轨迹?
用 evo 将估计 c2w 与 GT 对齐,计算平移 APE RMSE。单目模式允许尺度校正。
Q25:如何评测渲染?
在非关键帧上 render,与 GT 计算 PSNR、SSIM、LPIPS,避免只测用于优化的关键帧。
Q26:自定义数据最容易错什么?
pose 方向、深度尺度、RGB/BGR、resize 后内参、畸变模型和时间对齐。
Q27:如果实时帧率低,先优化哪里?
先关闭 GUI 和最终评测,记录 Gaussian 数量和显存;然后减少 tracking/mapping iterations、窗口大小和新增点密度。不要先牺牲位姿稳定性。
Q28:如何给 MonoGS 加回环?
需要地点识别、相对位姿验证、pose graph 优化。高斯应按关键帧或子图归属,回环后整体变换相关子图并重新局部/全局优化。
Q29:如何接入 IMU/LiDAR?
IMU 提供高频预测和初始化,LiDAR/RGB-D 提供尺度/几何先验。应在状态估计层融合,输出可靠相机位姿和深度,再改 Gaussian 初始化与损失,而不是直接把传感器数据塞进 renderer。
Q30:一分钟源码介绍模板
MonoGS 用 3D Gaussian map 做 SLAM 地图。slam.py 的主进程运行 FrontEnd,
并启动独立 BackEnd。前端对新帧以上一帧位姿初始化,调用带 pose gradient 的
Gaussian rasterizer,使用 RGB 或 RGB-D loss 优化 SE(3) 位姿增量和曝光;满足
平移与共视条件后建立关键帧。后端把关键帧 RGB 与真实/伪深度反投影为点云并
初始化 Gaussian,在滑动窗口联合优化高斯、最近关键帧位姿和曝光;根据屏幕
空间梯度 clone/split,根据 opacity、尺寸和共视次数 prune。前后端通过队列
同步 Gaussian、可见性和关键帧位姿,最后用 evo 和 PSNR/SSIM/LPIPS 评测。
28. 进阶练习
练习 1:验证坐标方向
从 TUM 数据读第一帧 pose,分别按 c2w 和 w2c 投影少量点,确认哪个版本与图像对齐,梳理 Camera.R/T、world_view_transform 与评测轨迹的关系。
练习 2:单目初始化消融
比较首关键帧常数深度的不同 scale/noise,统计 reset 次数、关键帧数、ATE 和最终 Gaussian 数。
练习 3:梯度 mask 消融
分别使用原 gradient mask、全图 mask 和更严格阈值,比较快速运动和弱纹理场景的位姿稳定性。
练习 4:关键帧策略消融
扫描 kf_translation、kf_overlap、window_size,画关键帧数、FPS、ATE、PSNR、Gaussian 数量的关系。
练习 5:Densification 消融
改变 densify_grad_threshold、gaussian_update_every、gaussian_th,分析地图容量、渲染孔洞、显存和运行时间。
练习 6:深度监督鲁棒化
在 RGB-D loss 中加入 Huber/Smooth L1、深度边缘降权或置信度 mask,比较反光/深度空洞区域的几何质量。
练习 7:自定义 RGB-D Dataset
使用自己的静态室内序列,先完成 Dataset loader 和首帧投影可视化,再连接 MonoGS。不要第一步就训练全系统。
练习 8:回环子图
为 Gaussian 保存 keyframe/submap owner,模拟 pose graph 校正后对相关 Gaussian 做刚体变换,观察跨子图边界和重新优化需求。
29. 参考资料
- MonoGS / Gaussian Splatting SLAM 论文:https://arxiv.org/abs/2312.06741
- MonoGS 项目主页:https://rmurai.co.uk/projects/GaussianSplattingSLAM/
- MonoGS GitHub:https://github.com/muskie82/MonoGS
- 原版 3D Gaussian Splatting:https://github.com/graphdeco-inria/gaussian-splatting
- Differential Gaussian Rasterization:https://github.com/graphdeco-inria/diff-gaussian-rasterization
- evo 轨迹评测:https://github.com/MichaelGrupp/evo
- Open3D 文档:https://www.open3d.org/docs/
相关本地资料:
- 3DGS_学习.md:3D Gaussian 表示、渲染、训练与 densification。
- Gaussian-Splatting_源码学习.md:原版 3DGS 源码结构。
- 相机内外参_学习.md:相机投影、坐标系和 COLMAP 位姿。
- IMU_学习.md:IMU、位姿和多传感器融合。
- 激光雷达_学习.md:LiDAR、深度和 3DGS 几何先验。
- GS-LIVM_源码学习.md:LiDAR-Inertial-Visual Gaussian Mapping。
总结
MonoGS 的核心不是"用 Gaussian 渲染一张图",而是把可微 Gaussian map 放进 SLAM 闭环:
Tracking 用渲染误差更新当前相机;
Keyframe 决定何时扩展地图;
Mapping 在局部窗口联合优化高斯和相机;
Densify/Prune 控制地图容量;
队列同步让前端和后端并行;
评测同时检查轨迹和新视角渲染。
读懂 MonoGS 后,下一步最值得做的是增强初始化、重定位和全局优化,再考虑接入 IMU、LiDAR 或深度先验。单纯增加 Gaussian 数量通常不能修复错误位姿和错误坐标系。