1. 从二维图像集合到三维点云的底层逻辑
第一次看到“从图像集合重建三维点云”这个方向时,我脑子里冒出的第一个疑问是:为什么非得用无监督学习?毕竟在三维视觉领域,有监督的方法已经能借助合成数据集(比如大量带标注的CAD模型渲染图)取得相当不错的精度。但真正上手做过几个项目之后,我才理解这个问题的核心痛点——标注数据太贵了。
你想想,要训练一个三维重建网络,如果走全监督路线,你需要为每一张输入图像准备对应的三维真值,也就是每个像素对应的空间坐标或者体素占用情况。这种数据在真实场景里几乎不可能大规模获取。激光扫描仪能扫出点云,但设备成本高、采集流程繁琐,而且扫描得到的点云和普通RGB图像之间还存在标定和对齐的问题。所以,当论文标题里出现“无监督学习”四个字时,它真正想解决的是:只用一组普通的二维照片,不依赖任何三维标注,让网络自己学会推断场景的三维结构。
这个任务在学术上通常被归类为“基于多视图几何的三维重建”与“自监督/无监督表示学习”的交叉地带。它的输入是一个图像集合——注意,不是视频序列,也不是经过精确标定的多相机阵列,而是一堆从不同角度拍摄的、可能连相机参数都不知道的散乱图片。输出则是这些图片所描述场景的三维点云。点云可以理解为空间中一堆离散的、带有坐标的三维点,每个点代表物体表面上的一个采样位置。和网格模型相比,点云没有面片连接信息,更轻量,也更适合作为后续处理(比如分类、分割、配准)的中间表示。
那么,无监督学习在这里到底“无”的是什么?严格来说,它不需要人工标注的三维真值,但通常需要利用图像之间的几何一致性作为监督信号。举个例子,同一场景的两张不同视角图片,它们对应的三维点投影到各自图像平面后,应该满足极线约束或者光度一致性。网络如果重建得准确,那么把重建出的三维点重新投影回任意视角,渲染出的颜色应该和原始图像很接近。这种“可微渲染+重投影误差”就构成了自监督信号。所以更准确的说法是“自监督”或“无三维标注监督”,但论文标题里用了“无监督学习”这个更宽泛的术语,我们也不必过于纠结字眼。
这个方向适合谁呢?如果你正在做三维重建、SLAM、神经渲染或者点云处理相关的项目,但苦于没有带标注的数据,那这套思路非常值得参考。即便你只是对“如何用深度学习从多张照片里恢复三维结构”感到好奇,理解它的核心机制也能帮你建立对三维视觉任务的基本直觉。接下来我会从论文的核心设计出发,拆解它的网络架构、损失函数、训练策略,并补充我在复现过程中踩过的坑和总结的技巧。
2. 无监督三维重建的网络架构拆解
2.1 整体流程:从图像集合到点云的三步走
这篇论文的架构可以概括为三个主要阶段:特征提取与匹配、深度与位姿联合估计、点云生成与优化。虽然不同论文的具体实现有差异,但核心思想是相通的。我以最典型的无监督多视图重建框架为例来说明。
第一阶段,输入一个图像集合,通常先用一个共享权重的卷积网络(比如ResNet的前几层)提取每张图片的特征图。这些特征图会被用来计算图像之间的匹配代价。如果相机位姿未知,还需要同时估计每张图片的相机参数(外参和内参)。这一步的难点在于:没有三维真值,怎么知道估计的位姿是对的?答案还是几何一致性——如果位姿估计准确,那么根据估计的深度图将一张图片的像素投影到另一张图片时,应该能找到颜色相近的对应点。
第二阶段,网络会为每张图片预测一张深度图。深度图是一张和原图尺寸相同的单通道图像,每个像素值代表该点距离相机的远近。有了深度图和相机位姿,就可以把每个像素反投影到三维空间,形成一张局部的点云。把所有图片的局部点云根据估计的位姿变换到同一个世界坐标系下,就得到了全局点云。
第三阶段是优化。由于初始预测的深度图和位姿都有误差,直接融合的点云会非常散乱。所以需要设计损失函数来联合优化深度网络和位姿估计。常见的损失包括光度一致性损失、深度平滑损失、点云倒角距离(Chamfer Distance)等。其中光度一致性是核心,它衡量的是“把一张图扭曲到另一张图的视角后,像素颜色差异有多大”。
2.2 为什么选择隐式表示而不是直接回归点坐标
很多早期的三维重建方法会直接让网络回归每个点的三维坐标,比如用PointNet之类的架构。但这类方法有个致命问题:点云是无序的。同一组点,换一个排列顺序,对网络来说就是完全不同的输出。虽然倒角距离等损失函数对顺序不敏感,但训练起来仍然很不稳定。
这篇论文的思路更偏向于基于深度图的显式表示。也就是说,网络不直接输出点云,而是输出每张图片的深度图,再通过几何投影生成点云。这样做的好处有几个:第一,深度图是规则网格,可以用标准的卷积网络处理,训练更稳定;第二,深度图天然携带了每个像素的对应关系,方便计算重投影误差;第三,通过多视图融合,可以逐步剔除离群点,得到更干净的点云。
我实测下来,这种“先深度后点云”的路线在无监督设定下确实比直接回归点坐标要稳得多。直接回归点坐标的方案,在训练初期很容易陷入所有点都塌缩到空间某一个小区域的退化解。而深度图方案因为有光度一致性约束,网络至少能学到一些有意义的表面结构。
2.3 相机位姿估计的耦合问题
无监督重建里最棘手的问题之一是:深度和位姿是耦合的。如果深度估计偏大,位姿估计也偏大,两者相乘后投影误差可能仍然很小,但实际的三维结构完全错了。这就是所谓的“尺度模糊”。在单目无监督深度估计中,这个问题尤其严重,因为单张图片无法确定绝对尺度。
论文通常采用两种策略来缓解:一是假设相机内参已知,只估计外参(旋转和平移),这样至少能固定一部分自由度;二是引入额外的约束,比如深度图的平滑性先验、或者利用多视图之间的三角化关系来提供尺度信息。我在复现时发现,如果完全不给任何尺度约束,网络确实会收敛到一个“看起来重投影误差很小,但点云尺度完全不对”的解。后来我加了一个简单的深度归一化步骤——每轮迭代后把预测的深度图除以当前batch的中值深度——训练就稳定多了。
注意:尺度模糊是无监督三维重建的固有难题。如果你的应用场景需要绝对尺度(比如测量物体实际尺寸),那无监督方法可能不够,需要引入IMU、双目基线或者已知尺寸的参照物。
3. 损失函数设计:无监督信号从哪来
3.1 光度一致性损失:核心中的核心
光度一致性损失的基本思想非常直观:假设场景是朗伯体(即表面反射不随视角变化),那么同一个三维点在不同视角下的颜色应该是一样的。基于这个假设,如果我把图像A的某个像素根据预测的深度和位姿投影到图像B的对应位置,那么两个像素的颜色值应该接近。
具体计算时,通常会采样一个局部patch而不是单个像素,因为单像素对噪声太敏感。比如取3x3或5x5的窗口,计算归一化互相关(NCC)或者绝对差之和(SAD)。论文里常用的是SSIM(结构相似性)和L1损失的组合,因为SSIM对亮度变化更鲁棒。
这里有个实操细节:遮挡区域的处理。如果图像A的某个像素在图像B中被遮挡了,那强行计算光度一致性就会引入错误梯度。常见的做法是计算前向和后向的光度误差,如果两者差异过大,就认为该像素被遮挡,将其损失权重置零。我在代码里实现了一个简单的遮挡掩码:对于每个像素,如果前向投影的误差大于后向投影误差的某个倍数,就屏蔽掉。这个技巧对最终点云质量提升非常明显,尤其是物体边缘区域。
3.2 深度平滑损失:让点云不那么“毛躁”
只用光度一致性损失训练出来的深度图往往噪声很大,因为纹理稀疏的区域(比如白墙)缺乏足够的约束。这时候需要加一个深度平滑损失,鼓励相邻像素的深度值相近。最常用的是二阶平滑项,也就是对深度图的梯度再做一次梯度,惩罚深度的二阶导数。
但平滑损失不能无脑加,否则会把物体的边缘也抹平。所以论文里通常会根据图像的边缘强度来调整平滑权重:在图像梯度大的地方(比如物体轮廓),降低平滑权重;在图像平坦区域,提高平滑权重。这个思路和图像去噪里的各向异性扩散很像。
我试过几种平滑损失的变体,最后发现基于图像边缘加权的L1平滑效果最稳。具体来说,平滑项写成:
# 伪代码示意 depth_dx = torch.abs(depth[:, :, :, :-1] - depth[:, :, :, 1:]) image_dx = torch.abs(image[:, :, :, :-1] - image[:, :, :, 1:]) smoothness_x = depth_dx * torch.exp(-image_dx)这样在图像边缘处,exp(-image_dx)接近0,平滑约束就放松了。这个技巧在KITTI等数据集上被广泛验证过,复现时可以直接抄。
3.3 点云倒角距离:直接优化三维结构
虽然光度一致性和深度平滑已经能work,但它们都是在二维图像空间或者深度图空间计算的,并没有直接约束最终的三维点云。为了进一步提升点云质量,论文还会引入倒角距离(Chamfer Distance)作为辅助损失。
倒角距离衡量的是两个点云之间的双向最近邻距离。假设预测点云为P,真实点云为Q(如果有的话),倒角距离定义为:
CD(P, Q) = sum_{p in P} min_{q in Q} ||p - q||^2 + sum_{q in Q} min_{p in P} ||q - p||^2
但在无监督设定下,我们没有真实点云Q。那怎么用倒角距离呢?一种做法是自一致性约束:把预测的点云分别投影到两个不同视角,得到两组深度图,然后比较这两组深度图生成的点云是否一致。另一种做法是多视图融合一致性:用不同子集的图像分别重建点云,然后要求这些点云互相接近。
我实际尝试过第一种方案,效果一般,因为投影过程本身就有误差,容易形成“误差抵消”的退化解。第二种方案更靠谱,但计算量翻倍。如果算力有限,建议优先保证光度一致性和深度平滑,倒角距离作为可选项。
4. 训练策略与实操中的关键细节
4.1 数据准备:图像集合怎么选
无监督方法对数据的要求比监督方法低,但也不是随便什么图片都能用。根据我的经验,图像集合需要满足几个条件:
- 视角覆盖足够:如果所有图片都是从同一个角度拍的,那三角化根本无从谈起。理想情况下,相邻图片之间的视角变化在10到30度之间。太小了基线不够,深度估计不准;太大了匹配困难,容易丢失对应关系。
- 光照尽量一致:虽然光度一致性损失对光照变化有一定容忍度,但如果同一场景有的图片是白天拍的、有的是晚上拍的,那颜色差异会直接破坏损失函数。如果无法避免,可以考虑在输入网络前做颜色归一化,或者用更鲁棒的损失(比如SSIM)。
- 纹理丰富:纯色墙面、玻璃幕墙、天空区域缺乏纹理,光度一致性约束很弱,重建出来的深度基本靠平滑先验猜。如果场景里这类区域占比很大,建议先做纹理增强或者引入额外的几何先验。
我在一个室内场景项目里用过大约80张手机拍摄的照片,视角覆盖了房间的四个角落和中间区域。训练出来的点云在沙发、桌子这些纹理丰富的物体上表现不错,但白墙区域就明显塌陷。后来我在白墙上贴了一些打印的纹理图案,重建质量立刻上了一个台阶。这个土办法虽然笨,但确实有效。
4.2 训练流程:分阶段优化更稳
端到端训练无监督重建网络很容易陷入局部最优。我推荐采用分阶段训练策略:
- 第一阶段:固定位姿,只训深度网络。如果你能先用COLMAP或者类似的多视图几何工具估计出相机位姿,那就先固定这些位姿,让深度网络专心学习光度一致性。这个阶段通常需要10到20个epoch。
- 第二阶段:联合优化深度和位姿。在深度网络有一定基础后,解冻位姿估计网络,用较小的学习率联合训练。这时候光度一致性损失可能会先上升后下降,属于正常现象。
- 第三阶段:加入点云一致性损失。最后几个epoch再加入倒角距离或者多视图融合一致性,进一步细化点云。
这个流程比直接端到端训练收敛得更快,最终点云也更干净。我试过直接端到端,训练了50个epoch,点云还是有很多离群点。分阶段之后,30个epoch左右就能得到可用的结果。
4.3 学习率与批大小的选择
无监督重建对学习率非常敏感。学习率太大,光度一致性损失会震荡,深度图出现大量噪点;学习率太小,收敛太慢,而且容易卡在“所有深度都差不多”的平凡解。
我的经验值是:如果batch size在4到8之间,初始学习率设在1e-4比较合适,然后用余弦退火慢慢降到1e-6。位姿估计网络的学习率要比深度网络小一个数量级,因为位姿的微小变化会导致投影位置的大幅偏移,梯度更容易爆炸。
批大小方面,由于每张图片都需要和其他图片计算光度一致性,显存占用和图片数量的平方成正比。如果显存有限,可以每次只采样一对图片计算损失,而不是所有图片两两组合。这样虽然损失估计的方差大一些,但训练仍然能收敛。
提示:如果训练过程中发现损失突然变成NaN,大概率是深度值出现了0或者负数。可以在深度网络输出后加一个Softplus激活函数,保证深度恒为正。另外,投影时的除法操作也要加一个极小值epsilon防止除零。
5. 点云后处理:从粗糙到可用的最后一公里
5.1 离群点剔除:统计滤波与半径滤波
网络直接输出的点云通常包含大量离群点,尤其是在物体边缘和纹理稀疏区域。这些离群点如果不处理,会严重影响后续的可视化和分析。最常用的两种滤波方法是:
- 统计滤波:对每个点,计算它到最近的k个邻居的平均距离。如果这个距离超过全局平均距离加若干倍标准差,就认为是离群点。k通常取20到50。
- 半径滤波:对每个点,统计以它为中心、半径为r的球体内有多少个邻居。如果邻居数少于某个阈值,就剔除该点。r需要根据点云的密度来定,一般取点云平均间距的2到3倍。
我一般先用统计滤波去掉明显的飞点,再用半径滤波清理稀疏区域的噪声。两轮下来,点云会干净很多。但要注意,滤波太狠会把物体表面的细节也去掉,所以参数需要根据具体场景调。我的习惯是先用保守参数跑一遍,可视化看看效果,再逐步收紧。
5.2 点云融合:多视图一致性的利用
如果网络为每张图片都输出了深度图,那么每张深度图都可以生成一个局部点云。把这些局部点云融合成全局点云时,简单的做法是直接根据位姿变换后拼接。但这样会保留大量冗余点,而且不同视图预测的深度在重叠区域可能不一致,导致点云出现“重影”。
更好的做法是基于可见性的一致性融合。对于空间中的每个三维点,统计它在多少个视图里被观测到,以及这些视图预测的深度是否一致。如果一致,就保留并取平均;如果不一致,就剔除。这个思路和TSDF(截断符号距离场)融合很像,只不过TSDF是在体素网格上做,而这里可以直接在点云上做。
我实现过一个简化版:把全局空间划分成小体素(比如1cm边长),每个体素内只保留一个点,该点的坐标取体素内所有点的均值。这样既能降采样,又能平滑掉一些噪声。体素大小需要根据场景尺度和点云密度来定,太小了起不到融合效果,太大了会丢失细节。
5.3 可视化与评估:没有真值怎么判断好坏
无监督重建最尴尬的地方是:你没有三维真值,怎么知道重建得好不好?我的做法是多角度重投影检查。把重建的点云渲染成深度图,然后和原始输入图片对比。如果渲染出的深度图在物体轮廓、平面区域都和原图吻合,那说明重建质量不错。如果渲染出的深度图边缘模糊、平面扭曲,那说明还有问题。
另一个实用的评估方法是留出验证视图。从图像集合里拿出几张图片不参与训练,只用它们来测试。把训练好的模型在验证视图上预测深度,然后和训练视图的重建结果做一致性检查。如果验证视图的深度和训练视图融合后的点云投影深度差异很大,说明网络过拟合了训练视图,泛化能力不行。
我还试过用COLMAP生成一个稀疏点云作为“伪真值”来评估。虽然COLMAP本身也有误差,但它的多视图几何约束很强,在纹理丰富区域的结果通常比无监督网络更准。把网络输出的点云和COLMAP点云做倒角距离对比,可以大致判断重建的精度水平。当然,这个方法只适合做相对评估,不能当作绝对真值。
6. 复现过程中踩过的坑与应对方案
6.1 投影函数的梯度回传问题
在实现光度一致性损失时,需要根据深度和位姿把一张图片的像素坐标投影到另一张图片。这个投影操作涉及可微分的网格采样(grid_sample)。如果直接用PyTorch的grid_sample,梯度会通过采样坐标回传到深度和位姿。但这里有个坑:当投影坐标超出图像边界时,grid_sample默认返回0,梯度也会变成0。这会导致边界区域的像素得不到有效监督。
我的解决方案是在计算损失时加一个有效的掩码,只对投影后落在图像范围内的像素计算损失。同时,对于边界附近的像素,可以适当降低损失权重,避免网络为了拟合边界而牺牲内部区域的质量。另外,grid_sample的padding_mode建议设为border而不是zeros,这样边界外的采样会复制边缘像素,梯度更稳定。
6.2 位姿参数化的选择
相机位姿包括旋转和平移。旋转的表示方式有很多种:欧拉角、四元数、旋转矩阵、轴角等。在无监督训练中,轴角表示通常比欧拉角更稳定,因为它没有万向节锁问题。但轴角在接近0度时梯度会变得很小,所以有些论文会用四元数加归一化。
我试过几种方案,最后发现用6D旋转表示(即用两个三维向量正交化后得到旋转矩阵)效果最好。这种表示在训练初期梯度充足,而且不需要额外的归一化约束。具体实现可以参考PyTorch3D里的rotation_6d_to_matrix函数。平移向量直接回归三个实数即可,但要注意尺度问题——如果场景尺度变化很大,最好对平移做归一化或者用对数尺度。
6.3 训练不收敛的排查清单
如果你复现时发现损失不下降或者点云完全乱掉,可以按以下顺序排查:
- 检查数据加载:图像是否归一化到相同范围?相机内参是否正确?如果内参错了,投影关系全错,网络不可能学到东西。
- 检查深度输出:深度值是否全为正?有没有出现NaN?可以在网络输出后加一个
torch.clamp(min=1e-3)。 - 检查投影坐标:投影后的坐标是否在合理范围内?如果所有点都投影到图像外,说明位姿或深度严重错误。
- 检查损失权重:光度一致性、深度平滑、点云一致性的权重是否平衡?建议先用光度一致性单独训练,确认能work后再加其他损失。
- 检查学习率:是不是太大了?可以试着降到1e-5看看损失是否开始下降。
我遇到过一次训练完全不收敛的情况,排查了半天发现是数据加载时把图像的RGB通道顺序搞反了(BGR当成了RGB)。虽然颜色通道反了理论上光度一致性仍然成立(因为两张图都反了),但预训练特征提取网络是在RGB上训练的,通道反了导致特征完全失效。这个坑很隐蔽,建议在数据加载后可视化几张图片确认一下。
6.4 显存优化的几个实用技巧
无监督多视图重建的显存占用主要来自两个方面:一是同时加载多张图片和对应的特征图;二是计算两两之间的光度一致性。如果显存不够,可以尝试:
- 降低输入分辨率:从512x512降到256x256,显存占用直接降为四分之一。虽然细节会损失一些,但训练速度大幅提升,适合前期调试。
- 梯度累积:用小batch训练,但每累积几个batch才更新一次参数。这样等效于大batch,但显存占用不变。
- 混合精度训练:用
torch.cuda.amp自动混合精度,显存占用可以减少30%到50%,而且训练速度更快。不过要注意,光度一致性损失里的SSIM计算对精度比较敏感,可能需要保持float32。 - 只采样部分视图对:不需要每轮都计算所有图片两两之间的损失,随机采样若干对即可。这样显存占用和图片数量成线性关系而不是平方关系。
7. 这个方向还能怎么玩:几个值得尝试的扩展思路
7.1 结合神经辐射场做精细化
无监督重建得到的点云通常比较粗糙,边缘不够锐利。一个自然的想法是把点云作为初始化,然后用神经辐射场(NeRF)做精细化。NeRF通过体渲染可以生成非常逼真的新视角图像,而且它的密度场天然编码了三维结构。你可以先用无监督方法快速得到一个粗略的点云,然后用这些点云初始化NeRF的采样空间,这样NeRF训练会快很多,而且不容易陷入局部最优。
我试过这个组合,效果确实比单独用NeRF好。NeRF单独训练时,在纹理稀疏区域容易产生“雾状”伪影,而有了点云提供的几何先验后,这些伪影明显减少。反过来,NeRF优化后的密度场也可以提取出更精细的点云,形成一个迭代优化的闭环。
7.2 动态场景的扩展
这篇论文讨论的是静态场景。如果场景中有运动物体,光度一致性假设就被破坏了。一个可能的扩展方向是引入运动分割,把动态物体和静态背景分开处理。静态背景用常规的无监督重建,动态物体则单独估计其运动轨迹和三维结构。这个方向在自动驾驶和机器人领域很有应用前景,但难度也大很多,因为运动分割本身就是一个病态问题。
7.3 跨数据集泛化
无监督方法的一个潜在优势是更容易跨数据集泛化,因为它不依赖三维标注,理论上可以在任意图像集合上训练。但实际中,网络在训练集上学到的先验(比如深度平滑的统计特性)可能在新场景中不适用。我试过在一个室内数据集上训练,然后直接用在室外场景上,结果深度图出现了明显的尺度偏差。后来加了一个简单的在线自适应步骤——在测试时用少量测试图片微调深度网络的最后几层——泛化能力提升了不少。这个思路类似于测试时增强,但需要小心不要过拟合到测试集。
7.4 点云下游任务的直接应用
重建出的点云可以直接用于一些下游任务,比如三维目标检测、语义分割、配准等。但无监督重建的点云通常比较稀疏且噪声大,直接喂给下游网络效果可能不好。我的经验是,先用统计滤波和体素降采样做预处理,然后把点云体素化或者投影到多视图,再用成熟的二维网络做检测或分割。这样虽然绕了一步,但比直接在噪声点云上训练三维网络要稳得多。
8. 一些个人体会
这个方向我断断续续跟了差不多一年,最大的感受是:无监督三维重建的瓶颈不在网络架构,而在损失函数的设计和训练策略的调优。你看论文里的网络结构其实都很简单,无非是卷积层加几个全连接层,但损失函数的每一项权重、每一个掩码的阈值,都会对最终点云质量产生巨大影响。我经常花一整天时间调一个损失权重,最后发现点云质量提升微乎其微,但换一个数据预处理方式,效果立刻就不一样了。
另一个体会是,不要迷信论文里的指标。很多论文在公开数据集上报告的倒角距离、F-score看起来很漂亮,但你用自己的数据一跑,发现完全不是那么回事。原因很简单:论文里的数据集通常经过精心筛选,光照均匀、纹理丰富、视角覆盖好。真实场景里的图像集合往往乱七八糟,这时候论文里的超参数基本都要重新调。所以我的建议是,先把论文的代码跑通,在它自带的数据集上复现出接近的结果,然后立刻换到自己的数据上,从数据清洗和预处理开始重新调。这个过程很痛苦,但绕不过去。
最后,如果你刚开始接触这个方向,我建议先从有监督的深度估计入手,比如用NYU Depth V2或者KITTI Depth数据集训练一个单目深度估计网络。理解了深度估计的基本流程后,再过渡到无监督多视图重建会容易很多。因为无监督重建的核心难点其实在于多视图几何约束的引入,而不是深度网络本身。先把单视图深度估计玩熟了,再往上叠多视图的复杂度,学习曲线会平缓不少。