自监督单目深度估计:用结构一致性突破光度损失瓶颈 做单目深度估计和视觉里程计的朋友应该都有这种体会想要真值数据又贵又难激光雷达深度不是处处都有纯靠图像亮度一致性做无监督学习又经常翻车。2018年那段时间我正好在研究从连续视频帧里无监督学习深度和姿态的框架也就是Monocular Depth与Pose联合自监督那一类。当时读到的《Leveraging Structure for Unsupervised Learning from Monocular Videos》让我印象很深它没有继续在光度误差上堆技巧而是明确提出利用结构——把场景几何结构当成监督信号的一部分去解决动态物体、遮挡、低纹理区域对自监督训练的干扰。这篇文章我把这套思路放在当时的技术脉络和我的实际复现经验里拆开讲适合正在做单目深度估计、视觉里程计、三维视觉的工程师和研究者如果你刚入门自监督学习也想搞清楚损失函数到底应该怎么设计这篇文章可以给你一个完整的参考路径。1. 自监督单目视频学习的基本盘从光度重建说起1.1 没有真值网络靠什么学单目自监督的前提并不复杂同一静态三维点在不同相机视角下投出来的像素亮度应该保持一致。这是整个自监督框架的地基。训练时我们从一段视频里取两帧一帧叫目标帧一帧叫源帧。网络分成两个头一个头预测深度图另一个头预测两帧之间的相对相机位姿然后用深度 位姿 相机内参把目标帧的每个像素投影到源帧视角重新采样出一张重建后的目标帧。如果深度和位姿都是对的重建应该和目标帧几乎一模一样差别越小损失越小网络就这样在完全没有深度真值的情况下学到了几何。这个过程本质上就是两帧之间的闭环验证。我在第一次实现时最惊讶的是这套框架对标注数据的依赖远低于预期只要相机内参给得够准不需要任何额外的人工标注就能训练出一套可用的深度估计器和视觉里程计。它是把多视角几何的物理约束直接当成了监督信号而不是从数据里去寻找统计规律。1.2 静态场景假设自监督框架真正的地基不过这套框架有一个很难绕开的前提场景必须是静态的画面里只能有相机本身的刚体运动。这里的静态指的不是画面不动而是没有独立于相机运动的物体。实际上车辆、行人、动物都在动遮挡边界也在变化。用动态场景去训练一个建立在静止假设上的损失函数一定会出问题。这个问题不是小麻烦而是根本性的。我在实测中观察到的典型现象是动态物体区域的光度损失无论如何都降不下来网络很快学会了和稀泥——把动态物体区域预测成一个大致的平均深度因为这种预测在重建时不会产生剧烈的不匹配损失虽然不是最小但梯度也小训练就很安稳。安稳不等于正确这部分深度几乎完全失真。也就是说像素层面的光度一致性只是外观上的看起来像而不是几何上的确实对得上。要打破这个困局就必须引入更深一层的结构约束这正是利用结构这个思路真正的价值所在。2. 纯光度损失会在哪些场景崩溃三类典型的梯度污染2.1 动态物体违反刚体假设的头号破坏者先说动态物体。它是所有自监督深度估计方法最先遇到的坑因为现实场景里根本躲不开。我跑驾驶数据集时最头疼的就是两侧不断驶过的车辆和路边行走的人。这类区域的重建错误明显但网络没有办法通过调整深度来解决——一个移动的车不能靠把深度改成某个值就静止下来。于是梯度在这些位置来回跳跃深度估计在这些位置变成一个模糊的平均平面。我后来养成了一个习惯训练到一定阶段后把深度图和原图叠在一起肉眼检查重点看车辆、行人轮廓处的深度锐度。如果轮廓边缘模糊得和背景连成一片十有八九是动态物体的梯度污染已经在起作用了。解决思路有两种第一种是在损失里降低这些区域权重第二种是给它们一个独立的几何验证通道。结构一致性提供的就是第二种。2.2 遮挡与暴露区域像素根本没有对应第二种毒药是遮挡和暴露区域。相机一动前景会把背景露出来或者把背景遮住。暴露出来的背景区域在源帧里根本不存在无论深度和位姿怎么调整重建目标帧时这个位置都找不到正确的颜色来源。这类像素如果全都参与损失计算模型等于在拟合一个不存在最优解的目标。更麻烦的是遮挡暴露通常集中出现在物体边缘而物体边缘又恰恰是深度估计最需要保持锐利的位置。我的经验是把这部分错误像素用validity mask过滤掉只保留有真实对应关系的像素参与光度损失。但这个mask怎么算、边界多宽直接影响效果。用几何结构误差来生成mask比单纯用光度残差更可靠因为它真正捕捉到了这个像素对应的三维点是否在两个视角里能被同一个刚体变换串起来。2.3 低纹理、重复纹理与光照变化外观匹配的盲区第三类问题来自外观本身。白墙、雪地、大面积玻璃幕墙局部窗口内亮度梯度趋近于零光度损失无法提供有效的约束栅栏、百叶窗、瓷砖之类的重复纹理则会让最小化过程陷入大量伪极小值而光照变化、反光和阴影会直接击穿亮度一致这条假设。我早期实验里最常看到的两种失败一是深度图在低纹理区域退化成一片平滑的灰因为在那个区域任何深度值都能重建出相似的亮度二是重复纹理区域深度发生纹理粘连深度边界贴着图像的重复花纹走而不是贴着真实几何边界。这两类失败说明只靠外观层面的采样一致性是不够的必须有不依赖颜色的几何信号参与。稀疏特征匹配和双视角深度结构对齐正是这种不依赖颜色的几何信号。3. 结构一致性是如何作为额外监督信号的3.1 双视角深度结构对齐光度约束的几何升级结构一致性这个思路第一次看懂时我觉得挺朴素但实际效果非常扎实。它的做法是不仅用目标帧的深度图去重建源帧视角的图像还要求把目标帧的深度图投影到源帧视角之后和源帧自己的深度图能够对得上。换句话说在颜色对得上之外再加一道深度结构对得上的硬性检查。直观理解把它想象成拼图两帧深度图是两块拼板深度和位姿都对的时候把其中一块平移到另一块的坐标系里两者的凹凸应该完全咬合。哪里咬合不上哪里就存在几何矛盾——动态物体、遮挡边界、反光区域都会在这里暴露。光度损失只约束亮度像不像结构一致性约束的是形状对不对后者在光照变化时依然稳定因为它压根不看颜色。我复现时给这个损失的设计如下源帧也经过同一个深度网络得到D_s然后把D_t结合位姿变换投影到源视角得到D_s_proj逐像素计算相对误差E_geo(p) |D_s(p) - D_s_proj(p)| / (D_s(p) D_s_proj(p)) L_geo mean(E_geo * mask_valid)这个误差图本身就有信息量它是后续软掩码的重要输入。实际测试下来加入这一项后模型在动态物体上的表现立刻会有肉眼可见的变化深度边界的锐利程度明显提升。3.2 动态区域与遮挡的软掩码生成有了几何误差图就可以把哪里不可信变成训练信号。软掩码的做法是M(p) 1 - exp(-beta * E_geo(p))E_geo大的地方M接近0光度损失在那里被抑制E_geo小的地方M接近1正常参与训练。这个mask比单纯用光度残差计算出来的mask更稳因为它度量的是几何矛盾而不是外观差异。比如一个深色的车在晚上可能和背景亮度差不多光度残差不明显但它的深度结构和背景深度在几何上对不上几何误差依然会抓住它。有个细节必须注意模型有偷懒倾向可能把整个画面都mask掉来逃避困难。所以我对mask本身加了一个稀疏正则鼓励M整体保持在较高水平只在真正矛盾的位置下降。这个约束我在第一版实验里漏掉了结果训练到后面模型几乎把所有区域都mask成零光度损失形同虚设。补上正则之后模型才被迫真正去解决动态物体问题而不是通过掩码绕过问题。3.3 稀疏特征匹配给姿态估计和深度加锚点结构信息不只有密集深度图一种形态。传统特征点匹配提供的是稀疏但非常可靠的几何锚点。ORB、SIFT这类描述子在一帧中找到关键点再在下一帧中找对应点本质上是在宣告这两个像素在三维空间里是同一个点。这个强先验对姿态估计特别有用哪怕光度损失在低纹理区域提供了任何有效梯度特征点重投影误差也能抬起姿态估计的方向。实现上我把特征匹配变成一项额外的重投影损失对每一对匹配点用PoseNet预测的位姿把它们从一帧投影到另一帧计算投影位置和实际匹配位置的误差让网络尽量满足这些几何约束。这相当于在坐标系的汪洋大海里插了一批固定的码头深度和位姿都不能漂离这些码头太远。在弱纹理室内场景中这个项带来的稳定性非常明显。4. 网络框架与损失函数的具体设计4.1 DepthNet与PoseNet的分工回到主干网络。DepthNet我采用标准的编码器-解码器结构编码器用残差网络解码器逐步上采样恢复全分辨率。输出的是逆深度图也就是1/Z而不是直接输出深度。原因很简单真实场景深度分布跨度极大几十厘米到上百米直接回归深度会让近处占的比重过大而逆深度天然把远近拉平训练更稳定。解码器部分我会用多尺度输出在多个分辨率上都预测深度因为几何结构一致性在小分辨率上更稳定而光度重建在全分辨率上更精细。PoseNet输入是将两帧图像在通道维拼接起来的6通道图经过几层卷积下采样最后输出6自由度的相对位姿三轴旋转角加三轴平移。这里有个容易被忽略的坑旋转角如果不做缩放初始梯度过大位姿会飞出去。我一般会对旋转输出乘一个0.01量级的缩放系数让网络从一个近似恒等的小位姿开始学效果会稳很多。4.2 重投影重建路径完整公式和可微采样重投影是整个自监督框架的闭环验证器。具体流程已知目标帧像素齐次坐标 p_t 由深度D_t(p_t)反投影到三维: P D_t(p_t) * K^{-1} * p_t 用预测位姿变换到源帧坐标系: P_s T_{t-s} * P 投影到源帧图像平面: p_s K * P_s / P_s.z 在源帧上用双线性采样读取重建像素每一步都必须可微深度、姿态、内参K全部要参与梯度回传。双线性采样本身是标准的网格采样操作PyTorch里可以直接用grid_sample实现。我踩过的坑是投影坐标落在图像边界外时grid_sample返回的是边界填充值重建成0产生的梯度会把网络往奇怪的方向推。解决方案是做一个validity mask只对落在源帧图像范围内的像素计算损失。4.3 四项损失如何合成一个可训练目标把各部分拼起来的最终形式大致如下L_total lambda_ph * sum(M * L_ph) lambda_sm * L_sm lambda_geo * sum(L_geo) lambda_feat * L_featL_ph是光度重建损失通常是SSIM和L1的组合SSIM占比高一点约0.85因为它对亮度变化更鲁棒L1则负责保持像素精度。L_sm是边缘感知的平滑损失在图像梯度小的区域加强深度平滑图像边缘处减弱平滑以保留几何边界。L_geo就是上一节说的双视角深度结构一致性。L_feat是稀疏特征点重投影损失可选。我的初始权重习惯是lambda_ph1.0lambda_sm0.2lambda_geo0.5lambda_feat视数据而定低纹理场景给到0.1左右。最关键的其实不是某个权重的绝对值而是几何项和光度项之间的平衡几何项太强模型会倾向于把一切困难区域都压成保守的平面光度项太强动态物体的梯度污染又回来。我在调参时通常是固定光度权重以0.1的步长扫描lambda_geo看验证集上动态物体区域的误差变化而不是只看全局误差。5. 训练与复现中的实际操作细节5.1 数据准备内参、帧对选择与尺度问题先说内参。自监督这类方法对相机内参K的敏感度超出很多人的想象。K不准相当于深度和位姿坐标系被同时扭曲最后估计出来的尺度比例、远近关系全部错乱。最气人的是这个错乱在loss上不一定体现得出来因为网络会把错乱消化掉体现在错误的尺度上。所以训练前务必检查内参标定如果实在没有标定我建议先基于图像宽高做一个粗略的针孔模型并对所有图像统一缩放至少在概率上不会偏离太多。帧对选择同样有讲究。一对帧间隔太小视差微弱重建信号弱姿态估计退化间隔太大外观差异、遮挡和光照变化都变大。我实践中推荐随机采样1到5帧间隔并随训练衰减采样窗口训练初期用大跨度让网络尽快建立几何概貌后期用小跨度精修细节。最后一个绕不开的问题是尺度。单目视频训练只能得到相对的深度尺度同一个三维点在哪个绝对距离上是不可知的。如果想做度量级应用需要在后续环节用已知物体的实际尺寸、或运动平视约束恢复绝对尺度。5.2 超参数选择如何快速判断训练是否健康判断训练健康不健康最忌讳只看loss曲线。自监督训练的loss下降不代表几何正确模型完全可以把动态物体区域平滑糊掉来降低损失。更可靠的判断方式是定期把网络输出可视化出来深度图、重建残差图、结构误差图、软掩码图四张图拼在一起看。我从第二版实验开始每500步存一次可视化结果。如果mask在车辆、行人区域变成暗色说明结构误差正在被识别如果mask几乎全亮说明模型还在跑纯光度学习如果深度图整体闪烁剧烈优先降低学习率。我用的学习率策略是Adam优化器、初始1e-4、每15个epoch乘0.5衰减batch size在16到32之间。如果显存紧张多尺度预测不必每条分支都在全分辨率计算只保留最小的两档计算结构和光度损失就够了。5.3 常见训练崩溃与排查思路把这几年踩过的坑列成清单方便排查深度整体漂移成均匀灰色通常是平滑损失过强、光度信号太弱先调低lambda_sm检查mask是否把大部分画面都关掉了。姿态估计发疯、旋转输出乱跳旋转参数没做缩放或平移权重过大。在输出层给旋转乘0.01系数训练一两百步观察角速度是否收敛。深度图上出现贴图纹理伪影深度边界跟着图像花纹走。这时加大一点平滑权重或者降低深度输出的分辨率再做上采样。训练初期不收敛先别急着调几何权重把光度损失和位姿重建单独拉出来跑确认重投影路径和双线性采样没有bug。这个确认很重要自监督框架里最常见的问题其实是坐标系的定义前后矛盾。6. 实验对比与边界条件结构约束的收益和局限6.1 公开数据集上的差异动态区域改善明显整体指标稳中有升这类方法在公开驾驶数据集上有一个非常一致的规律全局误差指标会有一定改善但幅度不大真正显眼的变化是把动态物体区域单独拎出来做评估时误差下降非常明显。这个规律在我自己的实验里也反复出现。只看全局指标你甚至会觉得结构一致性没什么用一旦把移动车辆、行人区域mask出来单独统计差距就出来了。所以如果读者朋友想复现这类结构约束我强烈建议评估阶段不要只报一个全局平均值至少增加两个子集动态物体区域、遮挡边界区域。结构一致性真正有用的时刻往往就是在这两个子集上体现出来的。我在写评估脚本时还专门用一个小分割网络离线标注了动态物体区域用于分析这个习惯帮我少走了很多弯路。6.2 仍然失败的场景和后续演化方向结构一致性并不是银弹。它的前提是相邻帧之间存在可追踪的视差如果摄像头快速旋转或场景本身平到没有纹理结构和光度信号会一起失效。纯旋转是最典型的失败场景——视差极小深度不可观测无论加什么损失都一样。彻底的遮挡、反射表面也会骗过结构误差因为没有真实对应关系可供校验。2018年之后这个方向很快就演进下去了主要分两条线一是把两帧约束扩展成多帧或时序约束引入递归结构和光流场让几何信息在时间维度上积累起来二是引入语义先验用场景理解去显式识别动态物体和遮挡而不是纯靠误差图隐式推断。现在回头看结构一致性依然是许多后续工作的底座没有几何校验这一步后面的光流、掩码、语义约束都缺少一个安全的锚点。如果现在你要复现这套利用结构的方法我最直接的建议是从两帧重建 双视角深度结构一致性 软掩码这个最小组合入手。先别急着加特征点重投影、多帧光流这些进阶项把最小组合在一个单一场景的小数据上跑通然后把深度图、结构误差图、mask图全部可视化出来确认动态物体区域确实被识别、被抑制了再逐步增加复杂度。我当年在这个项目上最大的体会是不要迷信loss也不要迷信全局指标。自监督学习最大的陷阱就是模型用各种方式作弊来降低损失而结构约束存在的意义就是给这个作弊的空间装上几何的栅栏。当你亲眼看到深度图在动态车辆区域不再糊成一团而是在物体边缘清晰切开你会觉得前面那些调参与排错都是值得的。希望这篇梳理能帮你少踩一些坑快一点看到那个清晰瞬间。