ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

3DGS自定义场景训练实战:从环境配置到效果调优全指南

3DGS自定义场景训练实战:从环境配置到效果调优全指南

很多朋友在看完3DGS的论文解析和效果展示后,第一反应都是"这渲染质量也太顶了",然后立刻想用自己的数据跑一遍。但真到了动手环节,环境配置、数据准备、参数调优这一整套流程,坑比想象中多得多。这篇系列第四篇,我就围绕代码实践这条线,把从零开始训练自定义场景的完整过程拆开讲清楚,包括我实际踩过的版本坑、数据采集教训、训练参数理解,以及最后几个典型问题的排查思路。适合已经大致了解3DGS原理、想真正动手跑通自定义数据集的朋友参考。

1. 动手前的账本:这套代码到底在做什么

1.1 输入与输出的对应关系

3DGS的训练流程本质上可以拆成一句话:给定一组同一场景的照片,先估计出每张照片的相机位姿,再在三维空间里铺设一堆可学习的高斯椭球,通过可微渲染不断调整这些椭球的位置、形状、颜色和透明度,最终让渲染出的图像和原始照片尽可能一致。

实际操作时,整个过程分成两个明显阶段。第一阶段是运动恢复结构(SfM),负责从照片中提取特征点、匹配、重建稀疏点云,同时算出每张照片的相机内参和外参。这一阶段的输出直接决定了后续训练能否收敛。第二阶段才是3DGS的训练,输入是原始图片加相机参数文件,输出是一组经过迭代优化的高斯模型数据,以及保存下来的点云、渲染图、损失曲线等中间产物。

很多人训练失败,并不是GPU算力不够,而是第一阶段就没做好。相机位姿估计错误、照片重合度太低、标定结果偏差明显,后面再怎么调学习率都救不回来。所以我会把SfM这一步称为整个项目的"地基"。

1.2 渲染过程的时间线

要理解训练中的各种报错和参数,还得知道渲染过程是怎么串起来的。每次迭代中,训练好的高斯集合会被投影到当前视角的相机平面上,利用光栅化计算每个像素的颜色。因为高斯椭球在二维平面上的投影是椭圆,颜色混合有可微的计算形式,才让反向传播变得高效。

训练过程中,系统会动态地分裂和克隆高斯椭球,让本来稀疏的区域变密,密集的区域变精。这个自适应操作对应到人眼观察就是:一开始场景是模糊的一团,随后边缘逐渐清晰,细节越来越多,最后整体变得锐利。理解了这个过程,后面遇到"训练到一半全是噪点""某些视角特别糊"之类的问题,就能快速定位是数据、参数还是迭代阶段的问题。

2. 环境搭建:版本组合比最新版本更重要

2.1 先守住性能底线

3DGS训练对硬件有一定要求,但没到普通用户完全碰不了的程度。官方基线项目在1080p分辨率下,单场景训练大概需要8GB以上显存才比较从容。如果显存只有6GB,也不是不能跑,只是分辨率得降下来,迭代轮次也要控制。

我用自己机器测试过几种显存组合,给个参考:

显存规模推荐分辨率迭代上限备注
6GB不超过800x6003万以内需要减小batch size或开启混合精度
8GB1080p3万到7万最常见的甜点区间
12GB及以上1080p以上7万到30万可以做高分辨率精修

另外,显卡驱动和CUDA版本的匹配很容易被忽略。我遇到过一个人装好环境后一训练就报"CUDA error: no kernel image",最后发现是驱动太旧,导致PyTorch编译时用的计算能力版本无法在显卡上跑。建议先查自己显卡的算力代数,再选择对应的CUDA和PyTorch版本。

2.2 Conda环境与依赖安装

推荐用Conda创建独立环境,不要直接装在base里。原因很朴素:3DGS项目对PyTorch、CUDA、编译器的版本耦合很强,独立环境方便隔离和推倒重来。

创建环境的命令通常长这样:

conda create -n gs_env python=3.10 conda activate gs_env conda install pytorch==2.1.0 torchvision==0.16.0 pytorch-cuda=11.8 -c pytorch -c nvidia

这里有几个关键点:

  • Python版本建议3.8到3.10之间。太新的Python版本,某些编译依赖可能还没有对应的预编译包。
  • PyTorch和CUDA版本要一起换,不能只单独升级PyTorch。否则大概率遇到算子不匹配的问题。
  • 原版3DGS依赖的自定义CUDA算子,编译时必须能找到和PyTorch对应的CUDA toolkit。如果你用的是conda安装的cuda,一定要把conda环境里的nvcc路径加入环境变量,不然编译时会用系统自带的旧版本。

2.3 编译安装的坑

3DGS项目里有一个很常见的步骤:编译自定义CUDA扩展。编译失败几乎成了新人劝退重灾区。我遇到的最高频错误包括:

  • 找不到cuda_runtime.h头文件。这种通常是CUDA环境变量没配对,CUDA_HOME指向了一个不包含头文件的目录。
  • 编译过程中报nvcc fatal。原因是CUDA toolkit版本和PyTorch要求的版本不一致。
  • fatal error: torch/extension.h: No such file or directory。这种情况一般不是缺头文件,而是PyTorch安装的路径和编译器搜索路径不一致,重新执行一遍PyTorch的安装通常能解决。

建议按"驱动 → CUDA toolkit → PyTorch → 编译扩展"的顺序排查,每次改完版本后,先把python -c "import torch; print(torch.__version__, torch.version.cuda)"确认一遍,再进下一步。

3. 自定义数据:照片采集与点云生成的完整链路

3.1 拍摄规则:什么样的照片能重建成功

大多数自定义场景训练失败,问题都出在照片采集阶段。3DGS对输入照片的核心要求是足够的视角重叠和稳定的光照条件。

先说视角重叠。相邻两张照片之间至少要保证60%以上的共同可见区域,最好是70%到80%。如果拍摄时每张照片都像拍照打卡一样随便按,前后画面没有重叠,SfM阶段就会因为特征点匹配失败而崩掉。我拍过一组建筑外墙照片,总共60张,其中十几张因为离得太远、内容过于重复,导致重建出的稀疏点云分成好几块,训练时背景疯狂飘浮,后来重新补拍才解决。

再说光照。同一个场景最好在阴天或在均匀漫射光下拍摄,避免强烈的阴影和过曝高光。3DGS虽然能处理一定范围的光照变化,但如果同一面墙在不同照片里分别是强光和阴影,高斯椭球会试图用半透明的"雾状"去补偿,最后渲染出来就是模糊一片。

拍摄时还要注意不要过度使用广角镜头。广角畸变太大,SfM特征匹配会变得困难。如果非要用广角,提前做畸变校正比事后靠算法硬扛要可靠得多。手机拍摄的话,关闭自动HDR、关闭美颜、锁定曝光和对焦,用视频连续扫过场景后抽帧,反而比一张张手动按快门更稳。

3.2 重建工具的选择与点云生成

拿到照片后,下一步是用SfM工具生成相机位姿和稀疏点云。社区最常用的方案是某款开源的三维重建工具箱,它在多视角几何重建领域几乎成了事实标准。如果你对这个工具不熟,可以先用它自带的图形界面跑一个小数据集,确认关键步骤都有输出后再转命令行。

命令行处理一个场景的典型流程是:

# 假设你已经把照片按顺序放在 images 目录下 m_tool feature_extractor --database_path database.db --image_path images m_tool exhaustive_matcher --database_path database.db m_tool mapper --database_path database.db --image_path images --output_path sparse

这里的三步分别是特征提取、特征匹配、稀疏重建。特征提取阶段会为每张照片生成关键点和描述子;匹配阶段在不同照片之间找到对应点;重建阶段通过迭代求解相机位姿和点云坐标。

第一次跑的时候,建议直接用默认参数。重建完成后,打开sparse/0目录,里面通常有三个文件:cameras.bin、images.bin、points3D.bin。这三个文件分别对应相机内参、图像位姿、稀疏点云。3DGS训练时真正用到的就是它们。

还有一个经常被忽视的点:原始照片的分辨率不要太高,也不要太低。3000万像素的原始照片直接喂给SfM,特征提取会非常慢,而且匹配结果不一定更准。建议先把长边缩到1600到2200像素之间。太低了(比如800)又会丢失细节,特征点数量不够,重建点云稀疏,训练出来的场景也容易缺少纹理。

3.3 目录结构:训练脚本默认怎么读数据

原版代码库对数据集目录结构有固定约定,如果不按约定放文件,脚本会在加载参数文件时报错。标准结构如下:

项目根目录/ ├── images/ # 图片 ├── sparse/0/ # SfM输出 │ ├── cameras.bin │ ├── images.bin │ └── points3D.bin

有些版本还支持把SfM结果转换成文本格式(.txt),但二进制格式读取更快、精度也更稳。如果你是在某个预处理流程中做转换,记得确认生成的images.bin里的相机模型和cameras.bin里的内参一一对应。有个不小的坑:用不同SfM工具输出的坐标系统可能不一样(比如旋转矩阵的坐标系是右手系还是左手系),这会直接导致训练出的模型方向颠倒或镜像。好在这个问题在3DGS的数据加载器里通常已经有处理,但如果发现训练出的场景是翻转的,优先检查这里。

4. 训练实操:命令、参数与日志阅读

4.1 训练命令与关键参数

环境配置好、数据目录也理顺之后,真正启动训练就只剩一条命令了。典型调用是:

python train.py --source_path /path/to/scene --model_path /path/to/output --iterations 30000
  • --source_path:场景根目录,代码会从这个目录里读取images和sparse文件夹。
  • --model_path:输出目录,保存中间模型、渲染图片、点云。
  • --iterations:训练总迭代次数。官方默认往往是3万或7万,取决于场景复杂度和目标准确率。

除了这三个,还有几个高频使用的参数需要理解:

参数含义我的经验
--save_iterations每隔多少轮保存一次模型快照别只用默认值,至少把30000、70000存下来,方便回溯
--test_iterations每隔多少轮跑一次测试评估和保存节奏对齐,能及时看到效果变化
--white_background是否使用白色背景室内/物体场景建议开,室外街景通常关闭
--sh_degree球谐系数最大阶数默认2阶,颜色表现复杂光照时够用,调高反而容易过拟合

训练过程会在终端输出每个迭代步的loss、渲染时间、存储占用等信息。第一次跑的时候,建议从--iterations 10000开始,先验证整个流程能通,再跑完整训练。很多朋友一上来就追官方的最佳效果,结果中途报错,调试成本远高于先跑个小规模试错的时间。

4.2 训练日志里的关键信号

日志中最值得关注的是loss值的下降趋势。前几百次迭代loss会快速下降,这是高斯椭球在快速分布到场景表面的阶段;之后进入缓慢优化的平台期,这是细节打磨阶段。如果你发现loss在几百步内降到很低,但渲染出来仍然模糊,说明数据或SfM阶段有问题,而不是训练不够。

训练过程中的rendering耗时也能反映问题。正常情况下,随着迭代推进,高斯数量会增加,单次渲染时间会逐渐上升。如果发现渲染时间异常早地飙升,多半是场景中生成了大量冗余高斯,也就是常说的"漂浮物",后面会专门讲。

另外一个很有用的习惯:定期打开model_path下的测试渲染图片看看。训练脚本会在指定迭代步把当前模型的渲染结果输出成图片。只看loss曲线容易自我欺骗,因为在3DGS里,loss值降到比较低,并不代表视觉质量一定好。有的场景虽然整体loss不高,但局部墙面糊成一团。所以图像检查比数值检查更重要。

4.3 输出文件解读

训练结束后,输出目录里最重要的文件是:

  • point_cloud/iteration_XXXXX/point_cloud.ply:导出的高斯点云模型,可以用常见点云查看器打开查看。里面不仅含坐标,还含每个高斯的颜色、旋转、缩放等参数。
  • test/和train/目录:存放渲染结果图像。
  • conf.pth、model.pth:模型权重文件,分别是配置和模型状态。

如果你要把模型用于实时交互展示,通常还需要将其转换为更紧凑的格式,社区有专门的转换工具,但这不是这篇文章的重点。我只提醒一句:point_cloud.ply里每个点是高斯椭球的中心,并不是传统意义上的表面点,所以拿它做点云分析时要区分清楚。

5. 效果调优:从故障现象倒推根因的排查思路

5.1 轮廓模糊与背景漂浮

这是新手最容易遇到的问题:背景和物体边缘总是有一层半透明的"雾",或者某些视角下出现飘在空中的碎片。绝大多数时候,这是SfM点云质量不够好造成的。

SfM生成的点云本身存在噪声点,3DGS训练时虽然有正则项抑制异常值,但如果点云里包含大量错误匹配点,训练会把这些错误点"巩固"成半透明的漂浮高斯。排查思路按这个顺序来:

  1. 在稀疏重建后,先用点云查看器检查点云,如果发现大量飞离场景主体之外的独立点,重新做SfM或剔除低质量匹配。
  2. 降低初始学习率,让高斯椭球更新更保守。
  3. 检查训练日志中高斯的增长速度,如果初始阶段增长过快,可以适当减少一些细分阈值参数(不同版本参数名不同,一般在代码里搜densify开头的参数)。

如果场景本身是室内小物体,用白色背景反而会帮着压制漂浮物。外景则要关闭白色背景,让它学习真实天空,不然会把天空区域强行渲染成白色大块,边缘泛着奇怪光晕。

5.2 过拟合到训练视角

训练完在训练视角上看效果极好,但换一个在采集时没怎么拍到的角度看,画面立刻模糊、撕裂或出现大量错位。这通常意味着视角覆盖不够密,网络根本没有条件学出这个角度的正确表达。

3DGS不是一般的多视角立体视觉算法,它对视角密度的要求甚至比NeRF还高。如果你在一个环形场景中只拍了三四十张照片,平均下来每个角度间隔大,模型就会把某个角度的高斯参数"背"下来,而不是真正理解场景几何。解决办法很简单:回去补拍,尤其是场景的角落、顶部、底部这些容易被忽略的区域。还有一个技巧:采集时走S形路线,不要绕一圈就收工。

5.3 显存不足时的降载方案

显存不足是训练自定义场景时最现实的拦路虎。常见报错是CUDA out of memory。降低显存占用,方法按优先级排列:

  1. 降低训练图像分辨率。采集阶段用了2200像素的图,训练时降到1600甚至1080,显存占用会显著下降。
  2. 缩小渲染图像块尺寸。很多版本的代码允许设置--image_size或类似的参数,否则默认按原始分辨率计算。
  3. 启用混合精度训练。PyTorch这块支持比较成熟,能省下约20%到30%的显存,不过偶尔会有精度损失。
  4. 减小batch size,或减少前向传播时同时渲染的视角数。

如果你已经降到上述措施仍不够,最直接的办法是换一块显存更大的显卡,或者接受训练分辨率低一点的事实,毕竟3DGS训练出的模型在推理阶段可以复用,训练分辨率太低只是影响上限,不是完全不可用。

5.4 光照变化场景的处理

室外场景最头疼的问题是照片在不同时间拍摄导致光影完全不一致。比如上午拍了一半,下午又去补拍另一部分,墙壁颜色和阴影全变了。3DGS的球谐函数虽然能表示视角相关的效果,但不能做到精确建模光照随时间的变化。

我的经验是:尽量一次拍完,最多不要跨度超过两个小时。如果实在没法一次拍完,回室内补光都比在阳光下硬刚强。另外,原始照片不要做太多后期调色,尤其不要让不同照片的白平衡差异太大,否则训练出的模型会带有明显的接缝色差。

6. 从单场景到批量实验:工程化管理经验

6.1 建立自己的项目目录规范

跑了几个场景之后,你就会发现整理实验有多重要。3DGS训练很吃目录、参数和数据的对应关系,稍微乱一点,过一个月自己都找不回当时的配置。

我的习惯是每个场景单独建一个目录,下面固定放三个子目录:

scene_name/ ├── raw_images/ # 原始照片备份,永不改动 ├── input/ # 缩放后的训练图像和SfM输出 ├── experiments/ │ ├── run_001_30k/ # 每次训练一个独立目录 │ └── run_002_60k/

每次训练启动前,把训练命令和参数写进一个run_config.txt,放在对应的experiments目录里。别依赖脑子记忆,也别只靠终端历史记录。等到做对比实验的时候,这份清单能帮你节省大量时间。

6.2 超参数调整的节奏

不要一上来就同时改学习率、迭代次数、密度阈值。我的建议是:固定一组基线参数跑通,之后每次只动一个变量,对比观察效果。比如先固定3万次迭代,看场景整体清晰度;再单独把迭代次数提高到7万,看细节提升是否明显。如果提升不大,问题更可能出在数据和SfM阶段,而不是训练量不足。

有一个比较隐蔽的经验:当场景有大量重复纹理或大面积无纹理区域时,增加迭代次数反而可能让墙面出现波纹状伪影。因为高斯为了拟合极细微的像素差异,会生成高频变化但几何上不稳定的结构。遇到这种情况,可以适当降低背景区域的透明度正则权重,或者减少高斯基数上限。

6.3 复现别人的效果前,先复现自己的

网上经常有人晒出惊艳的3DGS效果图。你在尝试完全复现前,建议先把自己已经跑通的场景作为基准,确保代码库、环境、数据格式完全一致。很多教程里的参数是针对特定场景调的,直接套用不一定适用。我在学习过程中试过把某个室外大场景的配置直接拿来做室内小物体,结果是训练完全发散。后来回到自己的基线场景,一点点对比差异,才弄明白问题不是配置好坏,而是场景特性差异导致的基础假设不成立。

所以,保持一个"最小工作示例"非常重要。哪怕只是一个简单的桌面物体或一堵纹理丰富的墙,只要能稳定训练出好效果,后续所有实验都有了可对比的锚点。


最后再分享一个小技巧:训练之前先花十分钟检查图像序列的可视化和SfM点云。照片里是否有抖动模糊、是否有大片天空空洞、点云是否覆盖了主要物体,这些问题在训练前发现只要几秒,拖到训练后就可能要重启几十次实验。3DGS真正花时间的不是在训练命令里调参,而是在数据采集和几何重建阶段把好关。希望这篇能帮朋友们少走一些弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进