做单目深度估计的同行,对MonoDepth2这个名字应该不陌生。这篇CVPR2019的论文,算是自监督单目深度估计方向真正意义上的“样板间”——后来自监督、自蒸馏、多帧融合那一大票工作,几乎都绕着它的框架转。我自己在不同项目里反复读过、复现过几遍这篇文章,每次都有新收获。这篇就按论文精读的思路,把MonoDepth2从设计动机、损失函数原理到复现细节完整拆一遍,给你一条可以直接照着落地的路线。
这篇文章适合谁读?如果你在做自动驾驶感知、三维重建、SLAM相关的项目,或者只是在学深度学习视觉,想找一个“没有真值标注也能训练出深度模型”的典型范式,这篇都很值得花一个下午好好啃。MonoDepth2解决的痛点是:单目深度估计传统上依赖大量带标注数据,标注成本极高,而它用视频序列的几何一致性做自监督,训练只需要普通RGB视频,这在工程落地里意义非常大。
1. 论文定位与研究动机:为什么MonoDepth2值得精读
1.1 单目深度估计要解决什么问题
单目深度估计的任务很直白:输入一张RGB图,输出每个像素对应的深度值。听起来简单,做起来非常难,难在三个地方。
第一,尺度模糊。同一个物体在大脑里可以同时存在“很近的小物体”和“很远的大物体”两种解释,单张图片没有尺度信息,模型必须靠大量先验才能猜对绝对深度。第二,低纹理区域。一面白墙、一片天空、一段柏油路,这些区域在图像里几乎没有任何可区分的纹理,算法很难判断它到底是平的还是斜的。第三,遮挡与运动。真实视频里总有车开过、人走过,前一帧还能看到的像素,下一帧就被挡住了,这给帧间几何约束带来很大干扰。
传统监督学习的方法,比如用深度传感器采集真值标定后的数据训练,效果确实不错,但标注成本高得离谱——用激光雷达扫描路面、逐帧对齐、过滤动态物体,一套流程走下来,人力和设备成本都远超一般项目能承受的范围。MonoDepth2换了一条路:不依赖任何深度真值标注,直接用视频相邻帧之间的几何关系来监督深度网络。训练时给网络一段连续行驶的视频,它自己就能从帧与帧的视角变化中“悟”出深度信息。推理时只需要单张图,非常干净。
1.2 从MonoDepth到MonoDepth2:四个关键改进的脉络
MonoDepth2不是凭空出现的。它的前身MonoDepth(也就是第一代)最早用双目图像的左右一致性作为训练信号,通过左图和右图之间的几何对应关系来估计深度。这个思路在当时已经很惊艳,但它有个硬伤:必须有双目相机,布设成本高,而且左右目基线短,远处误差大。
MonoDepth2把目光转向了更普适的视频序列。它引入一个位姿估计网络,从相邻两帧中估计出相机的相对运动,再配合深度网络实现“视图合成”式的自监督。框架看起来简单,但作者团队在细节上做了四个核心改进,每一个都能单独拎出来写一篇消融实验:
- 最小重投影损失:在多视角视图合成时,逐像素取重投影误差的最小值,而不是平均值,直接抑制了遮挡和运动物体带来的异常梯度。
- 自动遮蔽机制:让模型自己识别那些“不满足相机运动假设”的像素,比如同速行驶的前车、静止不动的画面,训练时把这些像素的损失屏蔽掉。
- 全分辨率多尺度估计:改变了前代作品在低分辨率上累加损失的做法,改成全分辨率下计算损失,显著减少“纹理复制”伪影。
- 数据增强的批判性实验:作者特意做了数据增强实验,发现单纯的色彩增强反而让精度下降约10%,这个反直觉的结果在论文里也写得很坦诚。
这些改进单独看都不算惊天动地,但组合起来,配合严谨的消融实验,让MonoDepth2成了一个难以绕开的基准。后续无数工作都是“基于MonoDepth2改一个模块”起步的。
1.3 为什么它能成为自监督深度估计的“样板工程”
我个人觉得,MonoDepth2能火这么多年,不只是因为精度高。精度高的方法多了,能成为基线的没几个。它的厉害之处在于“结构干净、设计可解释、组件可替换”。
先说结构干净。整个框架只有两个网络:深度网络负责输出深度图,位姿网络负责输出帧间相对位姿。训练时两者联合优化,推理时只保留深度网络,部署成本非常低。
再说设计可解释。论文里的每一个模块——最小重投影、自动遮蔽、全分辨率多尺度——都有明确的物理直觉和数学动机,不是靠玄学调参堆出来的。你可以清楚地解释为什么min比mean好,为什么mask能挡住动态物体,为什么全分辨率能消除纹理复制。
最后是组件可替换。因为框架解耦干净,后续工作可以像搭积木一样,只换其中一个模块来验证新idea:换更强的backbone、换更精细的位姿估计、换更好的光度误差函数,甚至把单帧扩展成多帧,都很方便。这种“可插拔”的架构设计,值得所有做算法工程的人学习。
2. 核心细节解析:四项关键设计的技术解剖
2.1 最小重投影损失:一条公式解决遮挡与运动物体
自监督深度估计的核心思路是“视图合成”。假设你有两帧相邻图像,已知了深度和相机位姿,就可以把前一帧的像素投影到后一帧,重采样出一张“合成的后一帧图”。如果深度和位姿都估计得准,这张合成图和真实后一帧应该高度相似。于是训练目标就变成了:最小化合成图和真实图之间的差异。
但这里有个致命问题:相机运动不是全局一致的。场景里有别的车在动,有行人走过,有树枝摇晃,这些物体不满足“世界静止、只有相机在动”的假设。对于这些区域,视图合成的几何关系是错的,如果强行计算损失,梯度会把深度网络带偏。
MonoDepth2的做法是逐像素取所有源视图重投影误差的最小值。公式上就是:
L_p = min_i pe(I_a, I_proj(i))其中 pe 是光度误差函数,I_proj(i) 是第 i 个源视图合成出的图像。直觉上,这个min操作的意思是:对某个像素来说,只要存在任何一个源视图能把它解释得好,就认为这个像素是可靠的,取这个最小的误差作为监督信号。这就像一个团队里,只要有一个成员能把问题答对,就相信这个答案是对的,而不被其他答错的成员带偏。
如果用mean或者median,结果都会差不少。mean会被最大的异常误差污染,median虽然有一定鲁棒性,但梯度特性不好,而且对多视图的支持不友好。min操作带来的梯度只回传到那个“最能解释该像素”的视图上,其他视图的异常梯度被自然屏蔽。这在遮挡场景下效果非常明显。
光度误差本身的构成也值得一提。论文用的是SSIM和L1的组合:
pe(I_a, I_b) = α * (1 - SSIM(I_a, I_b)) / 2 + (1 - α) * ||I_a - I_b||_1α默认取0.5。SSIM负责衡量图像的结构相似度,对光照变化和轻微偏移更鲁棒;L1负责约束像素级别的绝对误差。两者结合,既有结构层面的宏观约束,又有像素层面的精修作用,比单纯用L1或者L2效果好得多。L2误差对异常像素过于敏感,一个离群点就会把梯度炸掉,所以在光度误差里一般不用。
2.2 自动遮蔽机制:让模型学会忽略“不配合”的像素
有了最小重投影损失之后,大部分动态物体和遮挡问题已经被缓解了,但还有一个bug:当相机自身不动时,或者场景中存在与相机同速运动的物体时,最小重投影会找到一种“偷懒”的退化解——什么都不预测也能让误差很小。
想象一下这样的场景:车堵在路上,相机基本没动,前后帧几乎一模一样。此时无论深度网络输出什么值,视图合成出来的图像都和原图差不多,因为两帧本来就没区别。模型学到的深度就变成了一坨没有意义的常数。同样,如果你前面有一辆同方向同速度行驶的车,它在画面里的位置一直不变,深度模型也没法从帧间差异中获取任何关于它的深度信息。
MonoDepth2用自动遮蔽机制来解决这类问题。具体做法是给每个像素算一个二进制掩码,掩码为1的像素才参与损失计算:
- 对每个像素,分别计算它和源视图直接的光度误差(不做任何几何变换),以及经过深度位姿变换后的重投影误差。
- 如果直接光度误差比重投影误差还要小,说明这个像素无法通过几何变换被解释,它很可能属于静止或匀速运动的物体,将掩码置为0。
- 反之,如果重投影误差更小,说明几何变换确实起了作用,掩码为1。
这个机制名字叫“自动”,是因为不需要任何外部语义标签,完全由光度误差自发决定。训练前期模型还很粗糙,mask可能分得不太准;随着训练推进,深度和位姿估计越来越准,mask也会越分越干净。
这里有个容易忽略的细节:mask的计算是停止梯度的。也就是说,mask本身不参与反向传播,它不是靠梯度学习出来的,而是靠当前的前向预测“计算”出来的。这样设计的好处是训练稳定,避免了mask和深度网络之间的耦合振荡。
实际项目中,如果你发现训练出的深度图里,车辆区域整块都模糊不清,或者大片区域深度值异常平滑,大概率就是automask把那些动态物体全都mask掉了,失去监督导致。这种情况在车辆密集的城市道路场景里尤其常见。
2.3 全分辨率多尺度估计:伪影从源头消除
多尺度估计是深度估计网络很常用的设计——在不同分辨率的特征图上分别预测深度,既能捕获全局结构,又能保留细节。但MonoDepth2的作者团队发现了前代作品MonoDepth中的一个问题:它在低分辨率尺度上计算深度loss,然后通过上采样叠加到全分辨率输出上,导致最终深度图上有一种特殊的伪影,叫做“纹理复制”。
纹理复制是什么?简单说,深度图上会出现和RGB纹理完全无关的“幽灵纹理”。比如路面区域深度应该是平滑变化的,但深度图上却出现了路面裂缝、石子纹理的痕迹。原因是,低分辨率尺度上,图像下采样后保留的纹理信息仍然会被深度网络错误地“借用”到深度预测上——网络的深层特征既在编码语义信息,又在隐式地传递高频纹理,两者耦合在一起,就形成了这种伪影。
MonoDepth2的解法非常直接:不在低分辨率上计算损失,而是把每个尺度的深度预测都上采样到原始分辨率,在全分辨率下计算损失。这样,损失计算的监督信号始终是全分辨率的,没有“分辨率错配”的问题。
具体实现上,每个尺度输出的深度图(或逆深度图)都会经过一个双线性上采样,恢复到输入图像的尺寸,然后再计算与目标视图合成的误差。同时,为了控制计算量,每个尺度使用了不同权重的损失项,浅层(高分辨率)权重更大,深层(低分辨率)权重相对小一些。
这个改动看着微不足道,却直接消除了大半的纹理复制伪影。我自己在做图像质量检查时,对比过MonoDepth和MonoDepth2输出深度图的细节区域,差异非常直观——前者深度图上经常有细碎的“纹路”,后者干净利落得多。
2.4 深度网络与位姿网络的结构约定
MonoDepth2的深度网络沿用了编码器-解码器结构。编码器是ResNet18或ResNet50,在ImageNet上预训练过;解码器是逐层上采样的多尺度结构,输出多个尺度的逆深度图。作者在论文里特地做了ResNet18和ResNet50的对比实验——ResNet50精度略高,但ResNet18推理速度快很多。对于工程部署来说,ResNet18是性价比极高的选择,自动驾驶场景如果算力有限,它也是更实际的方案。
深度预测不是直接输出深度值,而是输出逆深度。最后一层用sigmoid激活函数,把输出压缩到(0,1)区间,再通过线性变换映射到0.1到100的深度范围:
disp = α * σ(x) + β depth = 1 / disp其中α取0.1,β取0.9。为什么要预测逆深度而不直接预测深度?因为深度数值分布严重不平衡——近处物体占少数像素,远处天空和地面占多数像素。逆深度这个变换把数值范围压缩到更均匀的区间,神经网络学起来轻松得多。而且最后接sigmoid,天然保证输出值是正的,不会出现预测出负深度的荒唐情况。
位姿网络的结构更轻量。输入是连续两帧图像在通道维度上拼接,经过一个ResNet18编码器,再通过全连接层输出一个6自由度的相对位姿向量(3个旋转角加3个平移量)。这个网络的精度不需要特别高——作者实验显示,它对最终深度精度的影响没那么大,但它的稳定性和收敛速度会影响整体训练质量。位姿网络只在训练时存在,推理阶段直接丢掉,做部署时完全不增加负担。
3. 复现实操:从数据集到训练参数的全流程笔记
3.1 环境、数据与预处理
MonoDepth2的官方实现是PyTorch的,单张GPU卡就能跑起来,不需要什么特殊硬件。官方推荐的配置是单卡训练,batch size为12。如果你的GPU显存不够(12G以下),可以适当调小batch,但要注意同步调整学习率。
数据方面,最有代表性的训练集是自动驾驶街景数据集KITTI。它提供了丰富的城市道路视频序列,非常适合MonoDepth2这种自监督范式。论文里用的是KITTI的raw数据,不需要用到深度真值标注的部分——这是自监督方法最吸引人的地方,训练集本质上就是一段段视频。
预处理有几个关键点需要注意:
- 训练输入尺寸通常是640×192,这个分辨率对应KITTI原始图像的高宽比,在训练时直接resize到这个尺寸。
- 数据拆分用的是Eigen标准拆分方式,区分训练集和测试集,这样不同论文跑出来的指标才能横向对比。
- 数据增强用的是随机水平翻转、颜色增强(亮度、对比度、饱和度、色相随机扰动)。有趣的是,论文里发现颜色增强反而降低了约10%的准确率——这个结果和直觉相反,作者在论文里专门写了出来,说明数据增强在自监督深度估计里不是万灵药,做项目时要谨慎使用。
3.2 损失函数与训练配置的落地细节
损失函数整体上有两级结构。第一级是前面讲的光度误差pe,用SSIM和L1加权组合,λ取0.5。第二级是跨尺度的累加——每个尺度都有自己的光度损失项,加权重后合并成一个总损失。具体来说:
- 多尺度输出的每个尺度计算一次光度损失,损失项在尺度间平均加权。
- 最小重投影和自动遮蔽都是在像素粒度上计算的,每个尺度的损失都独立应用这两个机制。
- 平滑项用边缘感知的深度平滑正则,保留深度不连续处的同时惩罚平坦区域的抖动。
在配置细节里,平滑项容易被忽略。它的作用是让深度图在纹理弱的区域变化更平缓,同时在边缘处保留突变。实现上是深度梯度的L1范数,再乘以一个基于图像梯度的权重,让平滑惩罚在有边缘的地方降低作用。这个正则项对最终深度图的质量影响很大——不加它,深度图会像噪点很多的毛玻璃;加太强,边缘又会被磨平。
优化器用的是Adam,初始学习率1e-4,训练大约20个epoch。学习率调度和一些训练技巧在不同代码版本里略有差异,跑官方代码时直接按默认配置就行。有一点要特别提醒:自监督训练对batch size比较敏感,batch太小的时候,帧间一致性信号不足,深度网络容易过早收敛到局部极小值,输出一片模糊的深度图。
3.3 一次完整的训练与评估流程记录
我按官方实现跑过一次完整流程,简单记录一下关键节点,给你做个参考。
第一步是准备数据。下载KITTI raw数据后,需要按序列拆分成训练样本,每一组训练样本包含连续的三帧(目标帧加两个源帧)。这个预处理脚本官方仓库里有,跑起来很方便。数据整理好后,训练集大约有三万多组样本。
第二步是训练。单卡batch size为12,输入分辨率640×192,初始学习率1e-4,用Adam优化器。训练过程中,深度网络和位姿网络是联合训练的,但两个网络的backbone都是预训练的ResNet18,不是从零开始。我的实测中,前几个epochloss下降很快,深度图从模糊到有个大概轮廓,大概一个小时后(单卡)就能看到像样的深度结构了。
第三步是评估。用标准指标来评估深度图质量:绝对相对误差(Abs Rel)、平方相对误差(Sq Rel)、RMSE以及阈值准确率δ(δ<1.25的像素占比)。论文里报告的结果在KITTI上,Abs Rel大约在0.115左右。如果你的复现结果差得太多,优先检查数据预处理和mask计算是否正确,这两个环节最容易出错。
消融实验也值得自己亲手跑一遍——把最小重投影改成求均值,深度图的动态区域立刻出现大面积空洞;把automask去掉,模型在车辆密集场景下会退化严重;把全分辨率多尺度改回低分辨率多尺度,纹理复制伪影马上回来。亲手验证这些对比,比看任何论文里的表格都更有说服力。
4. 复现常见问题、参数调优与踩坑实录
4.1 我在实际复现中遇到过的典型问题
做深度学习项目,纸上谈兵和实际跑通之间隔着一条深深的沟。MonoDepth2的复现也不例外,这里整理几类我在实际操作中真实遇到过的问题,以及对应的排查思路。
第一类:loss变成NaN。这个在自监督深度估计里很常见。我的排查路径是:先检查数据里有没有全黑帧或全白帧——这类图像的光度梯度几乎是零,很容易让优化器失控。其次检查学习率,如果初始学习率超过了训练阈值,前几个step就可能直接炸掉。最后检查sigmoid相关的数值范围,如果深度约束范围放得太大,梯度在极值附近容易不稳定。
第二类:深度图边缘发虚、远处一片糊。这个问题的根源通常是平滑项权重太大。平滑项惩罚深度梯度,权重过大会把边缘一起磨平,导致物体和背景之间没有清晰的分界。调低平滑项系数后,边缘质量会明显改善。
第三类:动态物体区域深度变成“黑洞”或“白洞”。这类问题多半出在automask和min重投影的交互上。如果动态物体占画面比例过高,mask很容易把所有动态像素都置为0,导致这些区域完全失去监督。这时候适当调整min重投影的源视图数量,或者给mask加一点松弛(允许小部分动态像素保留损失),会改善不少。
第四类:收敛速度极慢,训练十几个epoch深度图还是很糊。极有可能是batch size太小。自监督方法的梯度方差本身就大,batch太小会让训练像在暴风雨里开船,方向来回摇摆。调大batch(如果能放得下)或者学习率warmup,都能加速收敛。
4.2 复现问题速查表
| 现象 | 可能原因 | 排查与解法 |
|---|---|---|
| Loss为NaN | 学习率过大、数据含异常帧、数值溢出 | 降低学习率,剔除纯黑/纯白帧,检查深度范围映射 |
| 深度图整体模糊 | batch太小、训练epoch不足、平滑项过强 | 增大batch,延长训练,降低平滑权重 |
| 动态物体区域深度异常 | automask误屏蔽、min重投影覆盖不足 | 检查源视图数量,调整mask松弛策略 |
| 纹理复制伪影明显 | 多尺度损失计算方式不对 | 确认每个尺度都上采样到全分辨率再算loss |
| 远处深度严重高估 | 逆深度映射范围不合理、训练数据里远处样本太少 | 检查α和β参数,适当增加远景样本权重 |
| 位姿网络不收敛 | 输入帧顺序错乱、相对位姿表示错误 | 检查数据配对是否有重叠视野,验证位姿输出范围 |
这张表背后是我踩过不少坑换来的教训。特别是最后一条,位姿网络的问题往往被忽略,但它的输出一旦发散,深度网络再怎么调都救不回来。
4.3 把MonoDepth2用到自己的任务里,可以怎么扩展
读完论文不是终点,用它解决问题才是。MonoDepth2的框架扩展性很强,结合我自己的判断,给你几个可行的方向。
第一个方向是多帧融合。原始MonoDepth2只用相邻两帧做视图合成,但真实场景里连续视频有大量冗余帧可用。后续的ManyDepth等方法把多帧信息融合进来,在遮挡区域和边缘区域获得了更稳的结果。如果你的数据是连续视频而不是零散图像,这个方向值得尝试。
第二个方向是给automask加语义先验。automask本质上是在做“静态/动态像素”的二分类,完全靠光度误差羞别。如果你手里有现成的语义分割模型,可以把动态物体(车、人)的mask和automask融合起来,动态区域的深度预测会显著改善。
第三个方向是轻量化改造。如果你的目标是部署到嵌入式设备,ResNet18的backbone可能还是太重。可以试试用MobileNet系列的编码器替换,配合理想蒸馏,在精度损失可控的条件下把推理速度提上去。我自己做过类似的实验,在精度损失很小的情况下,推理速度差不多能翻倍。
第四个方向是面向特定场景的微调。MonoDepth2在街景上表现好,但换到室内、无人机航拍或水下场景,分布偏移会让效果大打折扣。好在自监督方法只需要重新采集视频就能微调,不需要标注,这是它工程落地时最大的便利。
结尾
说了这么多,回到最初的问题:精读MonoDepth2到底值不值?我的答案是相当值。它的代码质量高、实验设计严谨、框架干净,认真读透一篇,胜过走马观花看十篇水文。尤其是“用几何一致性替代真值标注”这个思路,即使未来出现了更强的深度估计方法,这种自监督范式依然会在很多数据匮乏的场景里发挥价值。
最后分享一个小技巧:如果你准备复现,别一上来就盯精度指标。先挑两三段有典型特征的视频(一段车多、一段直路、一段转圈),把训练过程中的深度图可视化出来看。看模型是怎么从一片模糊慢慢长出结构的,看动态物体是如何被mask处理的,看不同模块各自在“干活”时的表现。这个过程的收获,比最终那一串指标数字大得多。