第 6 章 重建与场景表达¶
第 5 章立了三维表达谱系与转换关系图,本章把其中"从图像/点云到网格、到高斯泼溅"这几条转换边逐条展开。重建的核心问题是"用最少代价得到下游能用的几何"——要碰撞进仿真是 mesh,要照片级回看是泼溅,要尺度先有是 SfM/MVS。代码基于 open3d 0.20.0;泼溅与 COLMAP/OpenMVS 的完整链路属外部生态、标「待核实」。
从图像到可交付网格的主链:
flowchart LR
img["图像序列"] --> sfm["SfM:相机位姿 + 稀疏点"]
sfm --> mvs["MVS:稠密点云"]
mvs --> mesh["泊松 / 体素场建 mesh"]
mesh --> clean["后处理与导出"]
1. 从图像到带尺度的点云¶
离线重建链的标准路径是「多视图图像 → 稀疏点云 → 稠密点云」。运动恢复结构(structure from motion,SfM)的管线:特征提取 → 两两匹配 → 相对位姿(对极,第 2 章)→ 增量三角化 → 捆绑调整(bundle adjustment,BA)全局精化。多视图立体(multi-view stereo,MVS)在已知稀疏点与相机位姿后,靠光度一致性把每格像素稠密化,产出稠密点云。
COLMAP 是 SfM/MVS 的常用实现(先跑 SfM 出稀疏点与位姿,再喂 MVS 出稠密点)。COLMAP/OpenMVS 的稀疏重建与稠密化导出形态标「待核实」;本章以合成多视角深度说明「图像 → 点云 → 网格」链路的几何内核。
深度图 → 点云的反投影(第 5 章谱系图的核心转换边)在 open3d 里一步到位:
pcd = o3d.geometry.PointCloud.create_from_depth_image(
o3d.geometry.Image(depth), K, np.linalg.inv(extrinsics)) # extrinsics 为 world->cam
反投影不丢点:产出点数等于非零深度像素数,深度图与点云只是同一信息的两种表示。open3d 的 integrate / create_from_depth_image 外参约定为 camera→world,与投影用的 world→camera 互逆,取反弄错会让整帧点云飞到大尺度错位。
2. 网格生成:三条路与自持体素场¶
2.1 三条路:TSDF、Poisson、BPA¶
网格生成的三条主流路线,输入与代价各不相同:
| 路线 | 输入 | 内核 | 适用 |
|---|---|---|---|
| TSDF 融合 + Marching Cubes | 多帧深度 + 位姿 | 体素存截断符号距离,融合后取零等值面 | 已知位姿的累积重建 |
| Poisson | 带法向点云 | 求解指示函数梯度匹配法向场 | 干净法向、需水密封闭 |
| 球面滚动(ball-pivoting, BPA) | 带法向点云 | 半径球在三邻点间滚动贴合 | 点密、要贴原点不封闭 |
TSDF + Marching Cubes(MC)。多视角球体场景,1 cm 体素、4 cm 截断带做融合(o3d.pipelines.integration.ScalableTSDFVolume;TSDFVolume 位于 pipelines.integration,不在 geometry):
vol = o3d.pipelines.integration.ScalableTSDFVolume(
voxel_length=0.01, sdf_trunc=0.04,
color_type=o3d.pipelines.integration.TSDFVolumeColorType.RGB8)
for rgbd, K, cam2world in frames:
vol.integrate(rgbd, K, cam2world) # 外参 camera->world
mesh = vol.extract_triangle_mesh() # 内部即 Marching Cubes 取等值面
覆盖不全时 extract_triangle_mesh 给出的网格边缘留洞、is_watertight 为 False。融合的意义在于摊薄噪声:不同位姿的观测在世界系同一格做加权平均,单帧的量化与测量误差被压低;代价是表面精度被体素尺寸量化(表面不确定度与体素边长同量级)。
Poisson。对球+平面点云(法向经 orient_normals_consistent_tangent_plane 一致化)跑 create_from_point_cloud_poisson(depth=8),返回网格附带 densities 指示重建可靠度。Poisson 天生造封闭曲面,会在数据稀疏处"脑补"出假面——按密度分位剔除低可靠区(如 p10 阈值)后再交付。
BPA。同一份点云 create_from_point_cloud_ball_pivoting(半径组 2/4/8 cm,入参为 utility.DoubleVector)不封闭、不脑补,贴着原始采样成面,代价是比 Poisson 慢一个数量级以上,且点密不足时会留大洞。三条路的选择:要封闭网格后处理做仿真走 Poisson+密度剔除;要严格贴合稀疏采样走 BPA;有多帧深度与位姿首选 TSDF(噪声被融合摊薄)。
2.2 自持体素场:marching cubes(scikit-image)¶
三条路都从"点云或多帧深度"出发;另一类场景是体素场已经在手上——扫掠管线自己累积了 TSDF/occupancy 的 3D 数组(而不是用 ScalableTSDFVolume),此时需要的只是"数组 → 等值面"这一步,skimage.measure.marching_cubes 正是这个过程的最小入口:喂一个 3D 标量场与 level(TSDF 取零等值面,occupancy 取 0.5),用 spacing 声明体素在三个轴上的物理尺寸,返回 (verts, faces, normals, values),前两项直接交给 trimesh.Trimesh(verts, faces) 或 Open3D 三角网格对象导出。
两条路的分界在职责归属:Open3D 的 TSDF 把"多帧融合 + 提取"绑定在一起;skimage 的 marching_cubes 只管提取,融合、坐标系、单位全由自己的管线负责——好处是能与已有体素管线解耦(裁剪、加权融合、多分辨率都可以在 numpy 里做),代价是这些都得自己写对。三条易错点:数组索引与物理坐标的轴序对齐(spacing 顺序随数组维序,不是想当然的 xyz);TSDF 的符号约定(内部为负还是为正)决定 level 取正还是取负;marching cubes 输出的是三角面片集合,未做简并面清理,导出前需自行过一道(Open3D 的 remove_degenerate_triangles 即可)。
3. 网格后处理与导出¶
3.1 抽稀与格式导出(Open3D 路线)¶
重建出的网格通常还要清理与瘦身。二次曲面简化(simplify_quadric_decimation)按目标面数抽稀,保持外形同时把三角数拉到仿真/渲染可承受。
导出侧,open3d 的 write_triangle_mesh 按扩展名分派,四种格式的差异落在顶点组织与法向上:
- PLY:二进制编码,顶点、面与法向精确往返;
- OBJ:存顶点与面,不存逐顶点法向,写出时需自行补;
- STL:无共享顶点概念,读回时每面独立复制顶点,顶点数按面数膨胀;
- glTF/GLB:读写往返依赖具体 ASSIMP 版本,跨工具交付前应核对目标环境的读回结果。
在版本差异之外,glb 的坑在于静默丢数据:读回失败时顶点与面可能都是空的,交付前必须检查读回的点面数,不能只看文件写成功。
3.2 抽稀的另一实现:fast_simplification 与凸包预算¶
fast_simplification.simplify(points, triangles, target_reduction=..., target_count=...) 是同一族二次误差简化器的数组式实现:直接吃 (n,3) 顶点与 (m,3) 面数组、返回抽稀后的数组,不经过 Open3D 的网格对象;target_reduction 是"删掉的比例"(给 0.9 表示删 90%),target_count 给目标面数,两者只能取其一。agg 控制二次误差聚合强度、preserve_border 保留边界、return_collapses 返回坍缩记录(可回放)。它适合管线里数据本就是 numpy 数组、或要把简化嵌进自己的批量处理循环的场合;写成 simplify_mesh(pyvista.PolyData) 时则直接链上 PyVista 生态。
两条预算要分开:渲染/仿真网格按像素与帧率定面数预算(抽稀到这里);碰撞侧不看抽稀网格,看凸包顶点数(mujoco-textbook 第 2 章「网格:mesh」的 maxhullvert 与 inertia=convex)或凸分解——同一份模型两边预算不同,不能拿一份抽稀结果同时交差。
4. 视觉里程计与激光 SLAM¶
直接法与特征法视觉里程计(VO)是第 2 章"相对位姿恢复"的实时工程化:特征法(ORB-SLAM 系列,稀疏特征 + 三重一致性)与直接法(DSO/SVO,最小化亮度误差、利用大基线)各一句名目,可用版本与许可标「待核实」;ROS 侧话题回引 ROS2-note 第 10 章 slam_toolbox / cartographer。激光 SLAM 与激光里程计(LIO)原理一句带过,实现同归 ROS2-note 第 10 章。
5. 高斯泼溅¶
三维高斯泼溅(3D Gaussian Splatting, 3DGS) 是当前照片级新视角合成的主流表达(第 5 章谱系图)。它与本章网格的关系是"竞争终点 + 可互转":
- 表达:一池各向异性三维高斯,每枚存中心、协方差(旋转+缩放)、不透明度、球谐颜色;本质是可优化的点云扩展(数据结构 \(G\times(3+6+1+\text{SH})\) 张量)。
- 初始化:标准路径是 SfM(COLMAP)先出稀疏点云与相机位姿,用稀疏点的高斯化做初值,再对多视图渲染损失做可微光栅化优化(这就是第 5 章谱系图的「图像 → 点云 → 泼溅」链中段)。
- 训练与致密化:优化中按梯度大处克隆/分裂高斯(densification)、剪掉透明高斯;训练需 GPU,具体实现(原始 3DGS /
gsplat/nerfstudio)的可用版本与许可标「待核实」。 - 泼溅 ↔ 网格:泼溅的几何是半透明"雾"、非确定表面;要得到可碰撞 mesh 需一步等值面类提取(SuGaR 一类方法),方法与工具名目待核实。反向 mesh→泼溅靠采样高斯化。渲染方程与光栅化的完整推导不在本板块范围,本章只立定义、转换关系与工具链位置。
查看与导出(SPLAT / .ply-of-gaussians 格式、SuperSplat/Postshot 类查看器)属外部生态,标待核实。
6. BA 与协方差直觉¶
6.1 BA 的全局精化与协方差¶
捆绑调整(bundle adjustment,BA)是重建链末尾的全局精化:把相机位姿与三维点当一堆待优化变量,最小化所有可见"重投影误差"之和,法方程是巨型稀疏系统(Schur 补降维求解)。协方差侧:优化收敛点的 Hessian 逆给出参数不确定性——三角化点越远、基线越短,深度方差越大(呼应第 2、4 章);这套"残差加权 + 协方差传播"的概率视角为 control-note 第 9 章的因子图/状态估计铺垫。
6.2 图优化后端与轨迹评测¶
BA 与回环的求解不是每帧从零写:工程上是把问题写成因子图(变量=位姿/路标,因子=重投影/里程计/回环约束),交给图优化库。三个常见后端:g2o(顶点-边框架,SLAM 教程与 ORB-SLAM 系常用)、GTSAM(贝叶斯因子图库,NonlinearFactorGraph + ISAM2 增量求解)、Ceres(非线性最小二乘通用求解器,slam_toolbox 的 CeresSolver 就是它)。
GTSAM 的最小用法足以看懂它的模型(pip install gtsam):
import gtsam
from gtsam import Pose2, noiseModel
noise = noiseModel.Diagonal.Sigmas([0.1, 0.1, 0.05]) # x, y, theta 三个方向的 σ
graph, init = gtsam.NonlinearFactorGraph(), gtsam.Values()
graph.add(gtsam.PriorFactorPose2(1, Pose2(0, 0, 0), noise)) # 固定起点
graph.add(gtsam.BetweenFactorPose2(1, 2, Pose2(2, 0, 0), noise)) # 里程计约束
init.insert(1, Pose2(0, 0, 0)); init.insert(2, Pose2(2.1, 0.1, 0))
result = gtsam.LevenbergMarquardtOptimizer(graph, init).optimize() # 一次性批处理
print(result.atPose2(2)) # 优化后的位姿
在线场景把 LevenbergMarquardtOptimizer 换成 gtsam.ISAM2:isam.update(new_factors, new_values) 后 isam.calculateEstimate(),回环进来时只重解受影响的部分。选型判据:批处理一次性解(离线重建、小规模 BA)用 Ceres 或 g2o;在线增量(回环后的全局修正)用 ISAM2。完整图优化理论不在本板块覆盖,这里只定“问题怎么建模、库负责什么”。
调完之后要回答“精度到底多少”:evo 把估计轨迹与真值对齐后算指标,命令行与 Python 两套入口:
pip install evo
# ATE(绝对轨迹误差):TUM 格式两文件,-a 做对齐、-v 打印统计、--plot 出图
evo_ape tum groundtruth.txt estimated.txt -va --plot
# RPE(相对位姿误差):固定间隔 Δ 上的相对误差,衡量局部漂移
evo_rpe tum groundtruth.txt estimated.txt -va --delta 1 --plot
两条纪律要写进报告:对齐方式要写明(APE 默认 SE(3) 对齐,尺度不可观时用 sim(3),-as;用 SE(3) 直接比单目轨迹会得出虚假的大误差),时间戳先做关联再算指标(时间偏差会伪装成轨迹误差,--t_max_diff 控制关联窗口)。
7. 要点¶
- 离线重建链「图像→稀疏点(SfM)→稠密点(MVS)→网格/泼溅」,COLMAP 是 SfM/MVS 的常用实现;COLMAP/OpenMVS/泼溅训练等外部生态一律标「待核实」。
- 深度反投影点数 = 非零像素数,信息量与深度图等价;open3d 外参约定 camera→world,取反弄错整帧飞掉。
- 网格生成三路各有输入与代价:TSDF 融合摊噪声、Poisson 封闭但会脑补、BPA 贴原点但慢一个量级以上。
- 网格交付前核对格式往返:STL 顶点按面复制膨胀、OBJ 无法向、glb 的 ASSIMP 往返版本相关且可能静默丢面。
- 泼溅是照片级竞争表达,靠 SfM 稀疏点初始化 + 可微光栅化优化;提网格需等值面提取,渲染推导不在本板块展开。
- BA 全局精化所有位姿与点,Hessian 逆给参数协方差;远处/短基线深度方差大,是概率侧估计时不确定性的来源。
参考文献与延伸阅读¶
- Schönberger, J. L., & Frahm, J.-M. Structure-from-Motion Revisited. CVPR, 2016. DOI
- Kazhdan, M., Bolitho, M., & Hoppe, H. Poisson Surface Reconstruction. Symposium on Geometry Processing, 2006. DOI
- Kerbl, B., Kopanas, G., Leimkühler, T., & Drettakis, G. 3D Gaussian Splatting for Real-Time Radiance Field Rendering. ACM TOG, 2023. arXiv
- Mildenhall, B., Srinivasan, P. P., Tancik, M., et al. NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis. ECCV, 2020. arXiv