1. 从一场顶会论文盘点说起三维重建在ICRA2020上的技术风向2020年6月23日ICRA 2020的论文列表在圈子里刷了屏。作为机器人领域最顶级的会议之一ICRA那一年因为疫情改成了全线上举办但论文的含金量一点没打折。我那天花了一整个下午把重建相关的论文标题和摘要过了一遍越看越觉得有意思——那一年的重建方向明显从“刷指标”转向了“真能用”。如果你正在做三维重建、SLAM、机器人感知或者单纯想了解这个领域在2020年前后发生了什么关键转折这篇盘点应该能帮你省下不少翻论文集的时间。先说清楚我关注的范围。ICRA本身是机器人会议所以它上面的重建论文和CVPR、ICCV那种纯视觉会议的路子不太一样。CVPR上的重建可能更关注单张图像、更关注渲染质量、更关注大规模数据集上的PSNR。ICRA上的重建几乎每一篇背后都站着一个具体的机器人任务抓取、导航、操作、探索。这意味着什么意味着这些方法必须考虑实时性、必须考虑传感器噪声、必须考虑计算资源受限的嵌入式平台。这个约束条件直接决定了那一批论文的技术选型。我大致把当时看到的论文分成了几个方向基于深度图的实时重建与融合、基于学习的单目/双目深度估计辅助重建、面向动态场景的重建、以及重建与语义理解的联合优化。这几个方向不是割裂的很多论文是交叉的但每个方向都有自己要解决的核心痛点。下面我按这个脉络把当时让我印象比较深的几类工作拆开来讲顺便补充一些我后来实际复现或者工程化时踩过的坑。2. 核心方向拆解那一年的重建论文到底在解决什么问题2.1 实时深度图融合从KinectFusion的遗产出发ICRA 2020上相当一部分重建论文骨子里还是KinectFusion那一套——用深度相机连续扫描把深度图融合到一个体素或者面元模型里。但2020年的工作已经不再满足于“小房间、慢速移动”的演示了。我印象比较深的是几篇做大规模场景实时融合的核心思路是分层体素哈希加移动体积。为什么是这两个技术因为体素网格的显存占用是O(n³)增长的。你重建一个房间分辨率1cm可能几GB显存就没了。要重建一栋楼纯稠密体素根本扛不住。分层体素哈希的做法是只在实际有表面的地方分配体素块空的地方不存。移动体积则是只保留相机附近的一块活跃区域远处的模型压缩或者换出到内存。这两个技术结合起来才能在嵌入式GPU上跑通大场景。我当时复现过其中一个开源实现参数调了整整一周。这里有个关键点体素块的大小和哈希表的冲突策略直接决定了重建的完整度。块太小哈希冲突多查询变慢块太大内存浪费严重。我实测下来8×8×8的块大小在1080Ti上是一个比较甜的平衡点。另外深度图的截断距离truncation distance也很讲究设成体素分辨率的3到5倍比较稳太小会导致表面出现空洞太大则会把不该融合的点也融进去表面会变“胖”。注意很多论文里不会写清楚深度图的预处理步骤。实际上原始深度图里的飞点如果不滤掉融合进去就是一堆悬浮的噪点。我一般会先做双边滤波再用一个基于法线一致性的离群点剔除这一步不做后面怎么调参都白搭。2.2 学习辅助重建当深度学习开始给几何“补课”2020年另一个明显的趋势是大家开始用神经网络来补传统几何方法的短板。传统方法在纹理缺失、重复纹理、透明物体这些场景下基本歇菜而学习的方法可以从数据里学到一些先验。ICRA 2020上有几篇工作是用单目深度估计网络给RGB图预测一个粗糙深度然后把这个深度作为先验去引导传统的PatchMatch立体匹配或者TSDF融合。这个思路听起来很合理但实操里有几个坑。第一尺度问题。单目深度估计网络输出的是相对深度没有绝对尺度。你直接拿去和双目或者结构光的数据融合尺度对不上模型直接崩。常见的做法是用稀疏的激光点或者已知尺寸的标定物去对齐尺度但这在机器人在线运行的时候很麻烦。第二域偏移。网络在训练集上表现很好换一个光照条件、换一个相机深度图的质量断崖式下跌。我当时试过一个在室内数据集上训得很好的模型拿到室外强光下预测的深度图几乎不能用。所以那一年的论文里比较务实的一类做法是把学习模块放在后端而不是前端。前端还是用传统的立体匹配或者深度相机出初始深度后端用学习的方法去做去噪、补全或者优化。这样即使网络泛化能力有限最差情况也只是退化成传统方法不会把整个系统带偏。这个设计思路我觉得比纯端到端的方法更适合机器人场景因为机器人系统对失效模式的要求很苛刻——你可以精度低一点但不能突然输出一个完全错误的结果。2.3 动态场景重建把“动的东西”摘出去机器人是在真实世界里跑的真实世界里全是动的人、动的物体。传统TSDF融合假设场景是静态的一有人走过模型上就留下一道“鬼影”。ICRA 2020上有好几篇专门处理动态场景的重建论文核心思路大同小异先检测动态区域再决定是剔除还是单独建模。检测动态区域的方法主要有两类。一类是基于几何的比如比较当前深度图和模型预测的深度图差异大的地方就认为是动态的。另一类是基于语义的用一个人体分割网络把人抠出来人所在的区域不参与静态融合。几何方法的好处是不依赖语义标签坏处是对快速运动或者遮挡变化很敏感。语义方法的好处是稳定坏处是只能处理训练过的类别而且分割网络本身也有计算开销。我实际用下来几何加语义的混合策略最稳。先用几何方法快速筛出可疑区域再用一个轻量级的语义分割网络去确认。这样既控制了计算量又降低了误检率。还有一个细节动态物体的深度图不要直接扔掉可以单独维护一个动态模型用于避障或者短期预测。很多论文只关注静态重建的质量但机器人真正需要的是对动态环境的理解这一点在选型的时候要想清楚。2.4 语义与几何联合优化重建不只是为了好看2020年还有一个让我眼前一亮的方向是把语义分割和几何重建放在一个优化框架里联合做。传统做法是先重建几何再在几何上跑语义分割两个阶段是串行的。联合优化的好处是几何的边界信息可以帮助语义分割把物体边缘切得更准语义的类别信息可以帮助几何重建在纹理缺失的区域保持表面连续。具体实现上常见的是在TSDF融合的时候每个体素除了存截断符号距离值还存一个语义类别的概率分布。每次融合新的深度图和语义图两个值一起更新。这样最后提取出来的mesh每个顶点或者每个面片都带着语义标签。这个表示方式对于机器人抓取和操作特别有用因为抓取规划需要知道物体的类别和位姿而不只是一个点云。不过这个方向的工程复杂度明显高一个量级。语义标签的噪声、类别不平衡、多帧融合时的标签冲突每一个都是坑。我当时试过一个开源实现光是调语义融合的权重就花了好几天。如果团队里没有专门做语义的人我建议先从松耦合的方案做起就是几何和语义分开跑后处理的时候再关联这样至少能快速出一个可用的版本。3. 关键技术细节与实操要点从论文到代码的距离3.1 体素哈希的参数选择与内存管理体素哈希是那一批实时重建系统的核心数据结构但论文里通常只给一个最终的内存占用数字不会告诉你这个数字是怎么来的。我拿一个具体的例子来算。假设体素分辨率是5mm重建一个10m×10m×3m的房间如果全稠密存储体素数量是2000×2000×600也就是24亿个。每个体素如果存TSDF值、权重、颜色至少16字节那就是38GB。这显然不现实。分层体素哈希的实际占用取决于表面面积。一个房间的内表面积大概是多少墙面加地面加家具算200平方米。每个体素块8×8×8覆盖的表面区域大概是(8×0.005)²0.0016平方米。200除以0.0016大概12.5万个块。每个块如果存512个体素每个体素16字节那就是12.5万×512×16大约1GB。再加上哈希表的开销和层级索引1.5GB左右。这个量级在桌面GPU上是可以接受的。但这里有个隐藏成本哈希表的动态扩容。当插入的块越来越多哈希表需要rehash这个瞬间的卡顿在实时系统里是致命的。我当时的做法是预分配一个足够大的哈希表比如按预估块数的两倍来分配然后用开放寻址法处理冲突。这样虽然浪费一些内存但避免了运行时的扩容操作。另一个技巧是把不活跃的块换出到CPU内存需要的时候再换回来这个换入换出的策略对帧率的稳定性影响很大。3.2 深度图预处理被低估的关键步骤我见过太多人直接拿深度相机的原始数据往融合管线里灌然后抱怨重建效果差。深度图的预处理至少包括三步无效值剔除、空间滤波、时域滤波。无效值剔除很简单深度为0或者超过量程的点直接扔掉。空间滤波我推荐用双边滤波因为它能在平滑噪声的同时保留边缘。参数上空间域的sigma设成2到3个像素值域的sigma设成深度标准差的2到3倍。时域滤波就是利用多帧的信息对同一个像素的深度值做加权平均。权重可以根据深度值的差异来定差异大的帧给低权重这样既能降噪又能适应场景变化。还有一个容易被忽略的点深度图的畸变校正。很多深度相机输出的深度图已经校正过了但如果你用的是自己标定的双目或者结构光畸变校正没做或者做得不准融合出来的平面会变成曲面。我当时的做法是先用棋盘格标定一遍把畸变系数存下来每次拿到深度图先做一次remap。这个操作的计算量不大但对精度的提升非常明显。3.3 学习模块的集成策略前端还是后端前面提到学习模块可以放在前端或者后端这里展开说一下具体的集成方式。前端集成就是让网络直接输出深度图或者法线图然后走传统的融合管线。这种方式的优点是管线简单缺点是网络的输出直接决定了重建质量没有兜底。后端集成是传统方法先出一个粗糙结果网络负责优化。比如用传统立体匹配出一个带噪声的深度图然后用一个去噪网络把它变干净。这种方式的好处是网络只需要学“去噪”这个相对简单的任务泛化压力小很多。我个人的经验是如果计算资源允许后端集成更稳妥。因为机器人场景的输入分布和训练集差异很大前端的网络很容易遇到没见过的场景。后端的去噪网络即使遇到没见过的噪声模式最差也就是去噪效果不好不会引入新的错误结构。另外后端集成对网络的实时性要求也低一些因为你可以隔几帧跑一次网络中间帧用传统方法顶着。3.4 动态物体处理中的阈值设定动态检测的核心是一个阈值当前深度和模型预测深度的差异超过多少就认为是动态。这个阈值设大了动态物体检测不出来模型上会有鬼影设小了噪声会被误判成动态静态区域的融合会变得很慢甚至停止更新。我的经验是这个阈值不能设成固定值要跟深度值挂钩。深度值大的地方深度测量的噪声也大阈值应该放宽。一个常用的经验公式是阈值 基础阈值 深度值的平方乘以一个系数。基础阈值大概设成深度分辨率的2到3倍系数根据相机的噪声模型来定。另外还可以引入一个时域的一致性检查如果某个像素连续多帧都被判为动态才真正把它标记为动态这样可以滤掉偶发的噪声引起的误判。4. 实操过程与核心环节实现一个可复现的实时重建管线4.1 系统架构与数据流设计我以当时复现的一个室内重建系统为例把整个管线的数据流讲清楚。输入是一路RGB-D数据分辨率640×480帧率30fps。第一步是深度图预处理包括畸变校正、双边滤波、无效值剔除。第二步是相机位姿估计我用的是ICP加光度误差的混合配准因为纯ICP在纹理缺失的区域容易跟丢。第三步是动态区域检测用几何差异加轻量语义分割。第四步是TSDF融合静态区域正常融合动态区域单独维护。第五步是表面提取每隔一定帧数跑一次Marching Cubes输出mesh。这个管线里位姿估计的稳定性是整个系统的命门。位姿一旦漂了后面的融合全错。我的做法是在ICP之外加一个基于特征点的重定位模块当ICP的残差突然变大或者跟踪丢失的时候用特征点匹配重新找回位姿。这个重定位模块不需要每帧都跑只在检测到异常的时候触发所以对实时性影响不大。4.2 关键参数的计算与选择过程体素分辨率的选择需要权衡精度和内存。我当时的场景是室内机器人抓取需要分辨出至少2cm的物体特征。根据奈奎斯特采样定理体素分辨率至少要设成目标特征尺寸的一半也就是1cm。但1cm的体素在10平方米的表面上会产生多少块算一下10平方米除以(8×0.01)²0.0064平方米大概1560个块。每个块512个体素每个体素16字节总共约12MB。这个内存占用非常友好。但如果场景扩大到100平方米就是120MB加上哈希表和层级索引大概200MB。仍然可以接受。所以我把体素分辨率定在了1cm。截断距离我设成了3倍体素分辨率也就是3cm。这个值的含义是只有距离表面3cm以内的深度测量才会被融合。设得太小表面会出现空洞设得太大不同表面的深度会互相干扰。3cm在我的场景里实测下来既能保证表面的完整度又不会把相邻的薄壁结构融在一起。4.3 动态区域处理的代码实现要点动态检测的代码逻辑大概是这样的对于每个像素先根据当前位姿把模型投影到当前视角得到模型预测的深度图。然后计算当前深度图和预测深度图的差异。如果差异大于自适应阈值并且该像素在语义分割结果中属于“人”或者“未知移动物体”类别就标记为动态。动态像素不参与静态TSDF的更新但会更新一个独立的动态体素网格。这里有个实现细节模型投影的深度图需要做插值。因为模型是离散的体素或者面元直接投影得到的深度图会有锯齿。我一般用双线性插值并且在深度不连续的地方做特殊处理避免把前景和背景的深度混在一起。这个细节论文里通常不会写但不做的话动态检测的误检率会很高。4.4 表面提取与后处理Marching Cubes提取出来的原始mesh通常包含大量的三角形直接用于机器人规划会太慢。我一般会做一个二次误差度量的网格简化把三角形数量降到原来的20%到30%同时保持几何误差在体素分辨率以内。简化之后再做一次拉普拉斯平滑去掉体素化带来的阶梯效应。平滑的迭代次数不要太多3到5次就够了太多会把尖锐特征也磨掉。还有一个后处理步骤是离群面片剔除。由于噪声或者动态物体的残留mesh里会有一些悬浮的小面片。我一般根据面片的面积和连通性来过滤面积小于某个阈值且不与主模型连通的面片直接删掉。这个操作对后续的抓取规划很重要因为悬浮面片会被误认为是可抓取的目标。5. 常见问题与排查技巧实录5.1 重建表面出现空洞或裂缝这是最常见的问题原因通常有三个。第一是深度图的无效值太多可能是相机距离太远或者表面反光。解决办法是调整相机的曝光或者换一个更适合的深度相机。第二是截断距离设得太小深度测量稍微有点噪声就落在截断范围之外了。解决办法是适当增大截断距离或者对深度图做更强的时域滤波。第三是位姿估计有微小漂移导致不同帧的深度图没有对齐。解决办法是检查ICP的收敛情况必要时加入闭环检测。5.2 动态物体留下鬼影鬼影的本质是动态物体的深度被错误地融合进了静态模型。排查的时候先确认动态检测模块是否正常工作。可以可视化动态检测的结果看看动态物体是否被正确标记。如果动态检测没问题但还有鬼影那可能是动态物体的运动速度太快在两帧之间移动了很大的距离导致几何差异检测失效。这种情况需要提高帧率或者用光流做运动补偿。5.3 系统运行一段时间后帧率下降帧率下降通常是因为哈希表里的块越来越多查询和插入的开销变大。解决办法是引入块换出机制把相机视野之外的、长时间没有更新的块换出到CPU内存。换出的策略可以基于时间戳比如超过30秒没有更新的块就换出。换出的时候要注意把块里的TSDF值和权重一起存下来换入的时候再恢复。另一个办法是限制活跃块的数量超过上限就强制换出最旧的块。5.4 语义标签在融合过程中冲突多帧融合的时候同一个体素可能在不同帧被赋予不同的语义标签。简单的多数投票效果一般因为不同视角的置信度不一样。我用的方法是贝叶斯更新每个体素维护一个类别概率分布每次新的观测到来时根据观测的置信度更新概率。置信度可以根据视角角度、距离、分割网络的输出概率来综合计算。这样即使某一帧的标签错了后续正确的观测也能把它纠正回来。5.5 常见问题速查表问题现象可能原因排查方法解决措施表面空洞深度无效值多检查深度图有效像素比例调整曝光或更换相机表面空洞截断距离太小可视化TSDF的截断区域增大截断距离至3-5倍体素表面裂缝位姿漂移检查ICP残差曲线加入重定位或闭环检测鬼影动态检测失效可视化动态掩码调整阈值或加入语义辅助帧率下降哈希表膨胀监控活跃块数量引入块换出机制语义冲突多帧标签不一致可视化语义融合结果改用贝叶斯概率更新表面阶梯体素分辨率粗观察mesh的锯齿程度提高分辨率或加平滑悬浮面片噪声残留检查面片连通性面积和连通性过滤提示排查问题的时候可视化工具比日志更管用。我一般会同时开三个窗口原始RGB图、带动态掩码的深度图、当前重建的mesh。这样一眼就能看出问题出在哪个环节。5.6 几个让我印象深刻的踩坑经历有一次我调一个开源的重建系统怎么调参数表面都有裂缝。后来发现是深度图和RGB图的时间戳没有对齐相差了大概两帧。深度图和RGB图来自不同的传感器硬件触发没有同步导致位姿估计用的RGB特征和深度图对不上。解决办法是用硬件同步线把两个传感器连起来或者用软件做时间戳对齐。这个坑我踩了整整两天因为裂缝看起来像是几何问题根本没想到是时间同步的问题。还有一次是重建出来的平面总是弯的。排查了很久最后发现是深度相机的畸变校正参数用错了。我用的标定文件是另一个镜头的焦距和畸变系数都不对。重新标定之后问题就消失了。这件事给我的教训是标定文件一定要和当前使用的硬件严格对应不能随便拿一个来用。另一个坑是关于GPU内存的。我一开始把哈希表的初始大小设得很大想着避免扩容。结果在嵌入式平台上GPU内存本来就不多哈希表占了一大块留给TSDF块的内存就不够了系统频繁换出换入帧率惨不忍睹。后来我把哈希表的初始大小调小允许它动态扩容但把扩容操作放在单独的线程里做避免阻塞主渲染线程。这个调整之后帧率稳定了很多。6. 从ICRA2020往回看哪些思路经得起时间考验6.1 分层体素哈希已经成为事实标准2020年之后我观察到越来越多的开源重建系统采用了分层体素哈希或者类似的稀疏体素结构。这个趋势在ICRA 2020上已经很明显了。稠密体素网格除了在很小的场景或者很粗的分辨率下还有用基本上已经被淘汰了。如果你现在要新做一个重建系统我建议直接从稀疏体素结构开始设计不要走稠密网格的老路。6.2 学习模块的定位越来越清晰ICRA 2020上关于学习辅助重建的论文当时还有些争议有人觉得学习的方法不够可靠。但后来的发展证明学习模块在重建管线里的定位越来越清晰它不是一个替代传统几何的端到端方案而是一个增强模块负责处理传统方法不擅长的子问题比如深度补全、去噪、动态检测。这个定位我觉得是对的也是工程上最可行的。6.3 动态场景处理从可选变成必选2020年之前很多重建系统默认场景是静态的动态处理是一个可选的加分项。但ICRA 2020上动态场景处理已经成了很多论文的标配。这个转变的背后是机器人应用场景的扩展机器人从实验室走向真实环境真实环境里不可能没有动态物体。如果你现在做重建系统还不考虑动态处理那基本上没法在实际场景里用。6.4 语义和几何的融合还在早期语义和几何的联合优化在ICRA 2020上是一个亮点但说实话当时的工作离工程化还有距离。主要问题是语义标签的噪声和计算开销。不过这个方向的价值是毋庸置疑的因为机器人真正需要的是语义地图而不只是几何地图。我个人的判断是随着语义分割网络的轻量化和鲁棒性提升这个方向在未来几年会有更大的突破。6.5 实时性和精度的平衡永远是核心ICRA 2020上的重建论文几乎每一篇都在讨论实时性和精度的平衡。这个平衡没有标准答案取决于具体的应用场景。抓取任务可能需要更高的精度导航任务可能更看重实时性。我的经验是先明确应用场景的精度底线和帧率底线然后在这个约束下尽量优化。不要盲目追求论文里的最高精度因为那个精度往往是在离线或者慢速条件下得到的在线跑不起来。7. 给后来者的实操建议如果你正在复现ICRA 2020上的某篇重建论文我建议先从作者的开源实现入手不要一上来就自己从头写。开源实现里有很多论文里不会写的工程细节比如线程管理、内存对齐、GPU核函数的优化这些细节对最终的性能影响很大。复现的时候先把作者提供的示例数据跑通确认环境配置没问题然后再换自己的数据。换数据的时候先跑一小段可视化中间结果确认每个模块的输出都符合预期再跑完整的数据集。参数调优的时候不要一次性调很多参数。我的做法是固定其他参数只调一个观察这个参数对结果的影响。记录每次调整后的指标和可视化结果建立一个参数-效果的对照表。这样调到最后你不仅知道什么参数好用还知道为什么好用。最后说一点关于评估的。重建的评估指标有很多比如精度、完整度、鲁棒性、实时性。不要只看一个指标。我见过一些系统在精度指标上很好看但帧率只有5fps根本没法用在机器人上。评估的时候要结合具体的应用场景设定合理的指标权重。如果可能的话在实际的机器人平台上跑一遍这比任何离线指标都更有说服力。我在实际项目里最大的体会是重建系统的瓶颈往往不在算法本身而在工程细节。时间同步、内存管理、异常处理这些看起来不起眼的地方往往是决定系统能不能稳定运行的关键。ICRA 2020上的那些论文算法思路都很漂亮但真正落地的时候拼的是谁把这些工程细节处理得更扎实。