
做了这么多年三维重建每次有人问Fusion类重建的核心算法我基本上都会先让他把TSDF吃透。别看TSDF这个名字听着高大上拆开看就是体素网格、有符号距离、加权重融合几个概念可它串联起来之后承担的任务比想象中重得多。这篇内容就专门聊TSDF算法它为什么能成为实时三维重建的标配体素、截断、权重融合这些概念到底在做什么以及在真实工程里参数应该怎么设、问题应该怎么排。三维重建Fusion中的TSDF算法通俗描述1. 项目概述三维重建Fusion和我为什么专门拆TSDF1.1 Fusion类系统到底在做什么先明确一个大背景。Fusion比如KinectFusion、ElasticFusion这一类实时重建系统要解决的核心问题可以概括成一句话拿着一个手持RGB-D相机在场景里走一圈不断把当前帧的深度图“融入”到一个统一的场景表示里最后得到一张完整的、可以直接渲染或者导出的三维表面模型。听起来像拍照拼图但实际难点在于每一帧都只是局部视角深度传感器本身有噪声、遮挡、甚至测量失败而且相机每时每刻都在动位姿估计还会不断累积误差。更麻烦的是几百帧深度图之间并没有严格的坐标对齐如果不做融合直接堆叠起来就是一团乱麻。很多刚接触的朋友容易把注意力全放在“怎么算相机位姿”上觉得只要姿态追踪做好了重建自然就准。这话对一半。位姿确实决定了几何能不能对齐但就算位姿非常好你手里的数据仍然是几百帧离散的深度图怎么把它们统一成一个连续、光滑、带置信度的表面是重建的后半程。TSDF解决的就是这个后半程问题把多帧观测在三维空间里做加权融合同时保留对“表面在哪里”的连续估计而不是简单地把点云堆到一起。1.2 TSDF在Fusion链路里的位置一套标准的Fusion流程可以拆成几步读取深度图做预处理降噪、补洞估计当前帧的相机位姿通常是ICP然后用位姿把当前深度图“投影”进全局的TSDF体素网格再根据融合后的几何信息辅助下一轮位姿估计。这个过程里TSDF夹在位姿估计与表面提取之间既是几何融合的容器又是后续建图、规划、渲染的基础。所以说TSDF不是一个独立的算法孤岛它跟位姿追踪是互相咬合的。位姿越准TSDF融合出来的表面越清晰TSDF表面的距离梯度信息反过来又能喂给ICP做更细致的位姿修正。我在实际项目里见过太多人只盯着某一个环节调参结果发现另一头又出问题根源往往就是没有理解这条链路是闭环的。提示如果你想搭一套自己的Fusion原型不要一上来就写GPU版TSDF。先用CPU把整个流程跑通观察每一帧的输出对不对再去看性能瓶颈在哪效率反而更高。1.3 TSDF相比直接拼接点云的优势这里就绕不开一个新手常见的疑问为什么不直接用深度图反投影成点云攒在一起再做配准最后用Poisson重建之类的方案出网格这样不是不行但和TSDF相比有明显的短板。第一点云是无序的不同帧测量到的同一点在空间里不会完全重合直接叠加会形成厚厚一层“毛刺面”需要额外做统计滤波、体素下采样来削薄。第二点云本身不记录每个点“离表面到底有多远”所以做表面提取、碰撞检测或者机器人路径规划时缺少一个能连续查询的距离场十分被动。第三多帧融合时TSDF有一套明确的权重体系可以根据观测角度、深度置信度来分配信任度这是单纯点云拼接很难做到的。再说得直白一点TSDF最终给到你的不是一个点集合而是一个隐式表面。表面方程在哪里为零哪里就是重建结果。这个数学性质让它后续接Marching Cubes、接光线追踪、接碰撞检测都非常顺滑。2. 核心细节解析TSDF是怎么把空间“刻”进网格的2.1 体素网格用“小方块”描述三维空间要理解TSDF先理解体素网格。所谓体素就是三维空间里的一个小立方体类似二维图像里的像素。重建的时候我们会选定一个体积范围比如3m×3m×3m然后用小立方体把这个范围均匀切分。假设体素边长是0.01m也就是1厘米那么一个方向上就有300个格子整体就是300³大约2700万个体素。这个数量级决定了TSDF天然是个吃内存的算法所以很多人问我TSDF为什么卡顿我第一步就是让他算自己到底填了多少体素进去。每个体素中心点都有一个世界坐标算法要做的事情就是不断更新这个点上的数据。如果只存一个布尔值表示“这里有没有东西”那就是传统的占用栅格它只能回答“有没有”回答不了“表面就在这附近哪个精确位置”。而TSDF升级的地方在于它存的是一个带符号的连续距离值这样后续提取表面的时候才能通过插值拿到更精确的坐标而不是只能在体素中心做粗糙的离散选择。2.2 SDF与TSDF的区别截断到底截了什么SDF的全称是Signed Distance Function带符号距离函数意思是空间中任意一点到最近表面的带符号距离点在表面外侧取正值内侧取负值绝对值等于到表面的距离表面本身恰好为0。这个概念在图形学、碰撞检测里都很常用数学性质非常好。但直接把SDF用在重建上有个现实问题远处体素的SDF值非常大计算代价高而且离表面太远的数据根本没有什么可信度。整个空间只有表面附近那一层信息是真正有用的远处那些值只会拖慢更新速度还可能引入噪声。于是TSDF里的“T”也就是Truncated、截断就派上了用场。具体做法是给定一个截断距离通常用记录值比如truncation表示当体素中心到表面的距离绝对值超过这个阈值时就不再更新它或者直接让它饱和为常量正负1。只有靠近表面的那层“薄壳”会被精确建模其他区域统统视为“已知空闲”或“完全遮挡”。这个截断看着简单却是整个算法能实时跑起来的关键。如果不截断任何一帧新观测都会影响远处大量体素更新量会爆炸根本没法在连续帧流里用。截断之后每帧只更新表面附近的一层体积计算量大幅下降表面的局部细节也不会被远处的无效观测污染。2.3 权重设计与融合公式为什么不能简单做平均现在到了TSDF最核心的公式。对第t帧某个体素中心的世界坐标是p先通过当前帧的相机位姿把它变换到相机坐标系得到深度坐标z_c再投影到图像平面取深度图对应位置的深度值d。然后定义当前帧的带符号距离sdf d - z_c如果sdf大于0说明体素在表面和相机之间也就是表面比体素的位置更远体素落在自由空间如果sdf小于0说明体素在表面后面被遮挡或者位于物体内部。接着做截断归一化tsdf clamp(sdf / truncation, -1, 1)当sdf绝对值小于截断距离时距离值被映射到[-1, 1]之间的连续值超出截断的部分直接饱和为±1。表面所在的位置就是tsdf跨越0的地方。实际操作中每个体素除了存TSDF值还要存一个权重w。融合公式长这样TSDF_new (W_old × TSDF_old w_current × tsdf_current) / (W_old w_current) W_new W_old w_current这个形式看起来就是加权平均但w_current不是随便取的。KinectFusion的原始工作里w_current和观测质量挂钩比如相机观察方向和表面法向夹角越接近垂直测量越可靠权重越大深度越远噪声越大权重越小。简单实现里也可以直接把权重设成常量1但这样会带来一个肉眼可见的问题如果相机停下来不动同一个表面被反复观测新帧权重固定旧帧占比被逐渐稀释表面位置会被最新一帧的噪声反复拉扯最后形成一种抖动的“呼吸感”。注意融合权重一定要设上限。常见做法是设一个最大权重比如20或50防止某个区域被一帧高质量数据彻底锁死导致后续信息完全进不来。以我的经验来看权重策略对最终质量的影响往往被新手低估。如果重建表面有“跟着相机走的拖影”多半是权重太简单要么没考虑观测角度要么权重饱和值设太低。真正落地时还需要根据传感器噪声模型去整定RGB-D相机和ToF相机的权重参数完全是两套玩法不能照搬。3. 实操过程与核心环节实现从一个简化TSDF实现说起3.1 参数选择体素尺寸、分辨率范围、截断距离怎么定先给一组我在实践中常用的参数范围后面讲解实现都基于这套设定。重建范围桌面级小物体验证用1m³足够如果扫室内房间通常用3m×3m×3m或更大。体素尺寸桌面级用0.002m到0.005m室内用0.01m就能看到足够好的细节再小就会明显压力增大。截断距离经验法则是取体素尺寸的3到5倍。体素1cm时截断距离取0.03m到0.05m比较合理。这三个参数互相耦合。体素分辨率越高表面细节越丰富但体素数量按三次方增长。比如范围3m³、体素1cm体素数是300³也就是2700万。每个体素如果存两个float分别给TSDF值和权重就是8字节总内存超过200MB。所以工程实现里要么用GPU显存来装网格要么用稀疏结构避开大片空区域。计算内存的公式很简单内存占用 三个方向格数的乘积 × 单个体素字节数。动手写代码之前先把这个数算出来能省去很多后期手忙脚乱。至于截断距离为什么不能乱设这里有个物理层面的原因。截断距离太小比如只有体素尺寸的1倍表面附近的距离场梯度太陡深度值稍微抖动零点位置就会来回跳截断距离太大自由空间和物体内部的距离被平均成一片模糊区域细微几何被糊掉。实际取舍只能靠实验但我建议第一次跑系统时固定体素尺寸把截断距离从3倍体素尺寸开始往上试看哪个值下表面最干净、孔洞最少。3.2 数据准备深度图、内参外参与坐标系约定TSDF融合不是拿一张深度图就能直接算你需要三类输入深度图、相机内参、相机外参。深度图本质是一张灰度图每个像素存的是相机到那个点的距离值单位是米。RGB-D相机会有无效像素比如值为0或NaN处理时一定要过滤否则会把零值当成一个极近的物体整个体素场被污染。内参就是相机的焦距和光心也就是fx、fy、cx、cy来自出厂标定或棋盘格标定。外参描述当前帧相机在世界坐标里的姿态通常是一个4×4矩阵T_cw作用是把世界坐标点变换到当前相机坐标系。坐标系约定是踩坑重灾区。我见过不少人把深度图和TSDF更新里的坐标搞反导致表面反向或者整片错乱。最好的习惯是全程用矩阵运算并且每一步都标注当前点处于世界系、相机系还是图像系。尤其在处理深度图时要记得深度图的坐标原点在相机光心而不是图像左上角所以反投影和正投影都有些容易忽略的技术细节建议写单元测试验证。3.3 关键实现步骤与伪代码下面给出的是CPU版实现虽然慢但逻辑最清晰是理解整个流程最快的路径。import numpy as np # 初始化体素网格nx, ny, nz 由重建范围和体素尺寸决定 nx ny nz 300 voxel_size 0.01 truncation 0.05 tsdf_grid np.zeros((nx, ny, nz), dtypenp.float32) weight_grid np.zeros_like(tsdf_grid) def integrate_frame(tsdf_grid, weight_grid, depth_img, K, T_cw): # K: 相机内参3x3 # T_cw: 当前帧外参4x4世界系到相机系 fx, fy K[0, 0], K[1, 1] cx, cy K[0, 2], K[1, 2] height, width depth_img.shape for ix in range(nx): for iy in range(ny): for iz in range(nz): # 体素中心由体素索引换算得到需要加上体素网格原点 voxel_center_world np.array([ ix * voxel_size origin_x, iy * voxel_size origin_y, iz * voxel_size origin_z ]) # 1. 从世界坐标变换到当前相机坐标 p_cam T_cw[:3, :3] voxel_center_world T_cw[:3, 3] if p_cam[2] 0: continue # 2. 投影到图像平面 u int(fx * p_cam[0] / p_cam[2] cx) v int(fy * p_cam[1] / p_cam[2] cy) if u 0 or u width or v 0 or v height: continue # 3. 读取深度过滤无效像素 depth_val depth_img[v, u] if depth_val 0: continue # 4. 计算有符号距离并做截断 sdf depth_val - p_cam[2] if sdf -truncation: tsdf_current min(1.0, sdf / truncation) w_current 1.0 else: tsdf_current 0.0 w_current 0.0 # 5. 加权融合 old_w weight_grid[ix, iy, iz] new_w old_w w_current tsdf_grid[ix, iy, iz] ( old_w * tsdf_grid[ix, iy, iz] w_current * tsdf_current ) / new_w weight_grid[ix, iy, iz] new_w这段伪代码基本就是TSDF的骨架。前三个嵌套循环遍历体素本质上是对每个体素独立完成“投影-采样-计算-融合”四件事数据之间没有依赖天然适合GPU并行。实际工程里我会把这个逻辑写成CUDA kernel一个线程处理一个体素帧率能从CPU的几帧跳到几十帧但核心流程和上面完全一致。权重更新里有个细节w_current为0时不能做融合否则分子乱、分母不变数值会出错。还有就是初始状态时TSDF值为0但权重也为0这个状态代表“还没有观测”后面提取表面时要靠权重阈值把它滤掉不然会得到一堆假表面。3.4 从TSDF到渲染MeshMarching Cubes等值面提取TSDF网格本身只是一堆距离数值看不出重建效果必须把“表面藏在哪个位置”提取出来。最常用的方法是Marching Cubes也就是行进立方体。它遍历所有体素单元看相邻体素的TSDF值有没有正负号翻转如果翻转就说明表面穿过了这个单元然后用线性插值求出交点再拼成三角形网格。处理完全部帧之后我对TSDF网格跑一次Marching Cubes提取阈值取0因为表面就对应当TSDF值为0的位置。输出的mesh可以保存成PLY或者OBJ格式用于渲染、3D打印或者后续几何处理。用Open3D、PCL或者VTK都有现成实现不需要自己从头写。提示提取Mesh之前一定要做权重掩膜处理。把权重低于某个阈值的体素标为无效区域Marching Cubes只在有效区域里跑否则那些权重为0的空洞区域会被错误地闭合生成大块假面。4. 常见问题与排查技巧实录4.1 表面毛刺和漂浮点新手最容易遇到的现象重建出来的表面坑坑洼洼甚至有一堆飘在空中的小颗粒。原因通常是两个方向一是深度图本身噪声大尤其是相机离物体太远的时候二是截断距离设得太小表面附近的TSDF值不稳定零点被噪声反复拉穿。排查顺序我建议这样先看单帧深度图判断是传感器原始噪声还是算法问题然后把截断距离往大调两到三倍观察毛刺是否减少最后检查融合权重确认每帧权重没有忽大忽小导致旧帧在几帧内就被彻底冲刷掉。实际操作中很多毛刺问题根本不需要上滤波算法只要把参数配合好就解决了。4.2 表面重影、漂移与位姿误差如果你重建出的表面像“双重曝光”一样有重影这时候问题大概率不在TSDF本身而在相机位姿。TSDF融合相当于把每帧深度都往空间里叠加位姿稍微偏差叠加出来的表面就会错开。重影越明显说明位姿误差越大。这就要回到Fusion链路里TSDF和ICP的默契了。ICP把当前TSDF当作参考几何模型来估计新帧位姿位姿更新后又拿新帧更新TSDF两者互相依赖。所以看到重影时我会先转头检查位姿估计有没有收敛比如看ICP的误差指标和帧间运动量而不是盲目调TSDF参数。一个很土但有效的办法是把连续两帧的TSDF快速渲染出来做视觉对比重影从哪一侧偏移一眼就能判断方向。4.3 内存爆炸与加速思路从稠密数组到GPU并行前面算过3m³、体素1cm的稠密网格就要200多MB但绝大多数空间其实根本没有被观测到全部用稠密数组存非常浪费。工程上常用稀疏TSDF结构比如VoxelHashing思路只分配被观测区域的块没数据的地方不占内存。ElasticFusion、BundleFusion这类系统的底层数据结构都和这个方向有关。如果暂时不想碰稀疏结构至少要把GPU并行用起来。TSDF更新是天然并行的每个体素的投影、采样、融合互不干扰。写一个CUDA核函数每个线程处理一个体素内参与位姿作为常量传入深度图作为纹理采样就能很轻松把性能拉上去。调算法阶段还可以用PyTorch把体素网格当3D张量用张量算子做批量投影和更新虽然优化的复杂度和显存占用都有代价但调试起来非常方便。4.4 实际调参的实测建议参数怎么设我直接整理一份实用清单。先把重建范围定得比目标物体大一圈就行别贪大范围越大单位体积内的体素越稀疏。体素尺寸先设成最终目标的两倍全流程跑通后再往细调避免一开始就被内存和速度卡住。截断距离从体素尺寸的3倍起调观察表面平滑度与细节丢失之间的平衡。最大权重建议从20开始试场景遮挡多的时候适当提高。深度图先做中值滤波或双边滤波预处理能明显改善边缘质量代价是每帧多几毫秒计算。这些值不是铁律但能让你在第一轮调试时不至于手忙脚乱。记住一个原则单个参数能解决的现象说明它本来就该影响那个环节如果调了半天问题还在多半是位姿或者数据预处理在捣乱别死磕TSDF本身。5. 从Fusion走向更多场景TSDF的影响范围与算法价值5.1 从KinectFusion到现代实时重建系统TSDF能成为标配不是因为某一篇论文带火了它而是因为它在“多帧深度融合”这个问题上做到了简洁、可扩展。后来的ElasticFusion引入曲面重建BundleFusion用全局到局部优化提升位姿精度底层核心依然是带符号距离融合的思想。很多商用手持扫描仪和移动设备里的室内扫描功能容积表示也没有跳出这个框架。这带来一个非常实际的好处一旦你把TSDF的原理吃透再去看这些新系统你会立刻抓住它们改进的切入点。比如某个系统说“我们改进了权重函数”你马上能猜出它优化的是边缘还是远距离说“我们换成了稀疏结构”你知道它是从内存和范围上做文章。5.2 机器人导航与自动驾驶里的距离场在机器人领域TSDF不完全是为了渲染好看的三维模型更多是用于导航和路径规划里的距离查询。规划算法需要不断问“这个点离障碍物有多远”“这个方向移动会不会碰撞”带符号距离场能直接提供距离和梯度方向对优化类算法非常友好。自动驾驶领域虽然更常用点云和BEV表示但在占据栅格预测、端到端几何感知等任务里TSDF也经常作为训练监督或者中间表示出现。从这个角度看TSDF已经不仅是三维重建专用算法而是一个稳定、可靠的距离场标准答案很多几何学习任务都在用它生成标注。5.3 TSDF的边界什么时候不该执着于TSDFTSDF也不是万能的。大范围室外场景或者大规模地图里纯体素表示成本很高这时候八叉树或者神经隐式表达更合适动态场景中物体会移动TSDF基于静态世界的假设就会失效遇到高光泽、透明表面深度传感器本身难以返回有效数据TSDF再强也救不了输入问题。所以我对团队里新人的建议是TSDF适合“环境相对静止、传感器能输出稳定深度、对实时性有要求、希望几何可解释可控”的任务。遇到这些边界情况先判断问题出在表示层还是数据层再决定要不要换方案。我做过的重建项目越多越觉得TSDF最大的优点是繁琐但可靠。它不像端到端学习方法那样“训练效果不错但归因困难”也不像简单点云拼接那样快但粗糙而是一个每步都有几何意义、每个参数都有物理含义的中间表示。你给够调参时间它能回馈稳定的输出你把它研究透了后面转NeRF、3DGS还是机器人建图都会多一层底层直觉。这次先聊到这儿。最后分享一个我坚持了很久的习惯每次调TSDF我都会顺手保存一张权重图它很诚实地反映每个区域的观测质量很多看似诡异的毛病盯着权重图看几分钟就有头绪了。希望这篇能帮你在三维重建的路上少走一些我走过的弯路。