MonoGS 源码学习

目标:读懂 MonoGS 从输入帧、相机跟踪、关键帧、局部建图到 3D Gaussian 增删和评测的完整链路;能跑通 TUM、Replica、EuRoC、Realsense 实操,能修改配置、定位问题,并能回答源码与系统设计面试题。

文章目录

  1. 先建立全局认识
  2. 项目定位与能力边界
  3. 本地源码快照与目录结构
  4. 环境、依赖与构建
  5. 运行入口与真实进程模型
  6. 总体数据流
  7. 配置系统精读
  8. [Dataset 与坐标系](#Dataset 与坐标系)
  9. [Camera 与位姿增量优化](#Camera 与位姿增量优化)
  10. [可微 Gaussian 渲染器](#可微 Gaussian 渲染器)
  11. GaussianModel:表示、初始化与优化器
  12. [Tracking 前端源码](#Tracking 前端源码)
  13. 关键帧与滑动窗口
  14. [Mapping 后端源码](#Mapping 后端源码)
  15. [Densification、Pruning 与可见性管理](#Densification、Pruning 与可见性管理)
  16. 损失函数与优化目标
  17. [多进程、队列与 GUI](#多进程、队列与 GUI)
  18. 评测、保存与输出文件
  19. [实操一:跑通官方 TUM 单目示例](#实操一:跑通官方 TUM 单目示例)
  20. [实操二:RGB-D、Replica 与单进程模式](#实操二:RGB-D、Replica 与单进程模式)
  21. [实操三:无 GUI 评测与结果检查](#实操三:无 GUI 评测与结果检查)
  22. [实操四:调试 Tracking、Mapping 和 Gaussian 数量](#实操四:调试 Tracking、Mapping 和 Gaussian 数量)
  23. 实操五:准备自定义数据集
  24. [实操六:实时 Realsense 与性能优化](#实操六:实时 Realsense 与性能优化)
  25. 推荐源码阅读路线
  26. 常见问题排查
  27. 面试常问问题
  28. 进阶练习
  29. 参考资料

1. 先建立全局认识

1.1 MonoGS 解决什么问题

MonoGS 将 SLAM 与 3D Gaussian Splatting 统一起来。它不先运行传统 SfM/COLMAP、再离线训练 3DGS;而是在输入图像序列时同时估计相机位姿、维护关键帧、增量构建 Gaussian 地图并渲染新视角。

复制代码
新帧 -> Gaussian 渲染 -> RGB/Depth 误差 -> 相机位姿更新
     -> 关键帧 -> 新高斯初始化 -> 局部 BA -> densify/prune

一句话:

复制代码
MonoGS 用显式可微 Gaussian 地图同时承担"可渲染场景表示"和"直接法视觉跟踪"的角色。

1.2 运行时闭环

  1. 读取 RGB、RGB-D、Stereo 或 Realsense 帧。
  2. 用上一帧估计位姿初始化当前相机。
  3. 渲染当前相机视角。
  4. 反向传播 RGB/Depth photometric loss,更新当前相机 SE(3) 增量和曝光。
  5. 根据相对平移与高斯可见性重叠判断关键帧。
  6. 后端将关键帧 RGB 与真实/伪深度反投影为点云,加入 Gaussian map。
  7. 在关键帧滑动窗口内联合优化高斯、最近相机位姿和曝光。
  8. 用屏幕空间梯度 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():

  1. 使用当前曝光参数修正 RGB。
  2. 选择外部 depthmap、真实深度或单目伪深度。
  3. 用 Open3D RGBDImage 和 PinholeCameraIntrinsic 反投影。
  4. 以当前 w2c 将点转换到世界相关坐标。
  5. 随机下采样并生成 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 剔除策略

  1. 前两个帧不轻易删除。
  2. 优先删除与当前帧共视很低的帧。
  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() 的主要流程

  1. 取 current_window 关键帧作为主优化视角。
  2. 收集窗口外旧关键帧。
  3. 渲染窗口内所有关键帧并累计 mapping loss。
  4. 随机额外采样最多两个窗口外视角,缓解遗忘。
  5. 加 scale 各向同性正则。
  6. 反向传播。
  7. 更新可见性、densification 统计和 Gaussian optimizer。
  8. 更新最近 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 数量爆炸的调参顺序

  1. 先检查位姿、深度和内参。
  2. 增大 pcd_downsample,降低新增点密度。
  3. 提高 densify_grad_threshold。
  4. 提高 gaussian_th 或增强 prune。
  5. 降低窗口或 mapping 次数做性能实验。
  6. 确认不是动态物体或错误深度触发持续高梯度。

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 五个高频错误

  1. 传入 c2w,但源码要 w2c。
  2. 深度毫米/原始 16-bit 没有除 depth_scale。
  3. OpenCV BGR 当成 RGB。
  4. resize/crop 后内参没更新。
  5. 畸变图像却使用未畸变 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 轮:全局

  1. README.md
  2. configs/mono/tum/base_config.yaml。
  3. slam.py
  4. utils/config_utils.py。
  5. utils/dataset.py。

目标:知道输入、配置、进程和输出位置。

第 2 轮:Tracking

  1. utils/camera_utils.py。
  2. utils/pose_utils.py。
  3. utils/slam_utils.py 的 get_loss_tracking。
  4. utils/slam_frontend.py 的 tracking 与 run。

目标:说明相机位姿为什么能从渲染损失中更新。

第 3 轮:高斯和渲染

  1. gaussian_model.py。
  2. gaussian_renderer/init.py。
  3. graphics_utils.py。
  4. diff-gaussian-rasterization 子模块。

目标:掌握 Gaussian 参数、投影、SH、opacity、2D gradient 和 pose gradient。

第 4 轮:后端

  1. BackEnd.initialize_map。
  2. BackEnd.map。
  3. BackEnd.run。
  4. GaussianModel.densify_and_prune。

目标:能够画出局部 BA、可见性和高斯增删顺序。

第 5 轮:工程

  1. eval_utils.py。
  2. gui_utils.py 与 slam_gui.py。
  3. multiprocessing_utils.py。
  4. 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. 参考资料

相关本地资料:

  • 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 数量通常不能修复错误位姿和错误坐标系。

相关推荐
金伟API10247 小时前
常见的SQL面试题:经典50例
数据库·人工智能·笔记·sql·学习
维克兜率天8 小时前
【维克】概率论入门:从“抛硬币“到理解金融市场的不确定性
经验分享·学习·概率论
世人万千丶8 小时前
鸿蒙Flutter Flex嵌套布局技巧
学习·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
笨鸟先飞的橘猫8 小时前
游戏后端分布式学习——开篇
分布式·学习·游戏
世人万千丶9 小时前
鸿蒙Flutter Flex布局性能优化
学习·flutter·性能优化·harmonyos·鸿蒙·鸿蒙系统
qeen8710 小时前
【C++】vector的模拟实现详解(二)
c++·学习·算法·迭代器·stl
YM52e10 小时前
鸿蒙 Flutter 渐变效果详解:LinearGradient、RadialGradient、SweepGradient
android·学习·flutter·华为·harmonyos·鸿蒙
YM52e11 小时前
鸿蒙 Flutter BoxDecoration装饰:打造精美UI效果
学习·flutter·ui·华为·harmonyos·鸿蒙
承渊政道11 小时前
【Python学习】(了解使用库、标准库、第三方库以及综合案例实操)
python·学习·pycharm·标准库·第三方库·使用库·综合案例