读深度估计论文就像给相机配眼镜,刚开始觉得能对上就行,后来才发现度数和瞳距才是命门。在无监督单目深度估计这个方向上,有一篇绕不过去的经典工作:用Left-Right Consistency(左右一致性)做训练约束的那篇论文。我第一次读它时,还没意识到这套思路会深刻影响后面一大批自监督深度估计方案——大家逐步发现:没有深度真值,完全可以用左眼右眼之间的几何关系来互相约束,训练结束后只输入单张图,照样能输出一张很稳的视差图。这篇笔记就围绕这个方法展开,适合正在入门无监督深度估计、以及打算在项目里复现和改造这类模型的人。
1. 真值是个奢侈品,但左右眼是免费的真值
1.1 监督方案的天花板与无监督的破局点
做单目深度估计这几年,我最深刻的体会是:障碍从来不在网络结构,而在真值。
如果我们走老路——监督学习,就需要每一个输入图像都有对应的像素级深度真值。室外场景里想要密集的深度真值,往往得靠激光雷达;但雷达点云投影到图像后是稀疏的,近处密、远处疏,而且车辆、行人边缘经常被遮挡,采集完还要花大力气做插值和对齐。室内倒是可以用结构光传感器,可一旦搬到开放环境,量程和光照又会让你怀疑人生。换句话说,大规模高质量深度真值是典型的奢侈品。
正是这种成本焦虑,逼出了无监督方向。破局点是:与其花大价钱获取深度真值,不如重新想一下“深度”这个变量从哪里来。一个很自然的便宜来源是双目立体图像对。两台相机固定基线、标定生产,同一空间点在左右图上的位置差异就是视差,而视差和深度是代数关系。两个视角之间天然存在几何约束,不需要人工标注。
但这里有个难解的地方:如果只是要在训练时用左右图做立体匹配,传统立体匹配算法已经做了几十年,为什么还要神经网络?因为目标不一样。传统立体重建要求在推理时也同时拿到左右图;而单目深度估计希望在推理阶段只有一个摄像头、一张图的情况下,仍然能从过往数据中学到“深度长什么样”的先验。也就是说,左右图对只是训练阶段的免费几何真值,网络的最终输出能力是单目场景下的深度估计。
1.2 立体对、视差与单目网络的奇怪组合
要理解左右一致性,先得把“视差”这个概念聊透。在理想的双目相机模型里,左右相机只存在水平方向的基线位移,那么空间点投影到左右两幅图像时,同一行的水平坐标会有一个偏移。这个偏移量叫视差d,近处的物体视差大,远处的物体视差小。由相机焦距f和基线长度B,深度z = f * B / d直接换算。
于是有了一个非常好的近似:如果某个像素的预测视差是对的,那么把这个像素按照视差移动到右图对应的位置,取到的颜色应当和左图原像素几乎一致。如果视差预测偏了,取到的颜色会错得离谱。于是深度预测问题变成了图像重建问题:预测视差图,把一张原图按视差重投影成另一张图,然后比较重建图和目标图像之间有多像。
这个方法的核心思想,就是用左右图的“互相可解释性”替代深度真值。网络在学什么?它本质上在学一个能同时满足两件事的视差图:一是把左图重投影成右图时像,二是把右图重投影成左图时像。两条重投影路径如果都能走得通,那么这张视差图基本上就是几何上自洽的。正是这个自洽约束,让网络在没有真值的情况下也能产生相当干净的深度图。
2. 网络在学什么:一张视差图与一条可微重投影通道
2.1 结构上的一处关键设计:同时看左右,却只输出一对视差
看原论文的结构时,我第一个注意到的是输入不是单张图。网络训练时输入的是左图和右图拼在一起的6通道图像,输出端则同时输出两个通道:左视差图D_l和右视差图D_r。这在当时看来是个很反直觉的做法——既然目标是单目深度估计,为什么训练时还要把右图喂进去?
原因恰恰就在“左右一致性”里。如果网络只输出左视差,那它只是从右图重投影左图,网络完全可以钻空子:它对右图不认识,所以只学会了“从右图采样出一个像左图的结果”,却不一定真的理解几何。而同时输出左右两套视差,再把两套视差之间的几何一致性作为额外损失,等于逼着网络同时从两个方向理解立体关系。左视差和右视差不再是两个独立的预测,而是互为镜像、互相校验的同一个三维结构的两种投影。
这种共享编码器的做法还有一个附带好处:左右两个视差图共享同一个特征空间,不会出现左眼学一套、右眼学一套的分裂局面。后来很多复现把它改成双分支共享编码器的结构,效果大体一致,但核心思想没有变——左右几何一致性必须被写进损失函数里,而不是指望网络自己领悟。
2.2 为什么输出的是视差而不是深度
论文里最值得琢磨的设计选择,是让网络输出视差图,而不是直接回归深度图。
第一个原因是尺度问题。深度值的变化范围非常大,从贴近镜头的几十厘米到远处的几百米,直接回归深度,网络在近处和远处的误差尺度完全不一样。而视差是归一化的像素位移,数值范围小且线性,更容易落入神经网络偏爱的激活区间。
第二个原因是形式匹配。左右重投影约束天然定义在视差上:重投影时使用的就是“某个像素移动了多少像素”,而不是“这个点有多远”。在视差空间里写重投影损失,不需要关心相机内参,也不需要每次计算前把深度换算成视差。
但有一个代价绕不开:视差图恢复出来的深度,天然带着尺度的模糊性。单张图像本身没有绝对尺度,一个近的小物体和一个远的大物体可以投影成同样大小。好在左右图训练时用了真实的立体基线,网络在训练中接触过真实的像素位移量,所以当测试相机的内参、基线和训练相机一致时,输出的深度能接近度量尺度。一旦换成另一套相机的配置,深度尺度就需要重新标定。这个坑我在实际测模型时踩过,后面细说。
2.3 可微重投影是这个方法能训练起来的最后一块拼图
前面说“按视差去另一张图采样”,听起来简单,真正实现时有个技术门槛:采样必须是可微的,否则网络拿不到梯度。
普通图像采样是按整数坐标取像素,比如(x, y)取源图坐标;但预测出的视差是浮点数,x - d大概率不是整数坐标。如果直接用四舍五入取整,梯度根本传不过去。论文使用的方案是双线性采样——一种可微的插值方式。采样点周围的四个像素按距离权重加权求和,权重对坐标的导数是可以计算的,于是误差信号能顺利回传到视差图上。
可以把这个过程想象成在老板的Excel表里用公式而不是数值:只要你改一个单元格,所有依赖它的单元格都会自动更新结果。双线性采样就是那个“公式通道”,它让整个网络变成一个大号的、可微的“视图合成器”:输入左右图,中间是预测视差,输出为重建图;重建图与真实图之间的误差,一路透过采样公式传回视差预测层。
这一步是整个方法能训练起来的决定性细节。如果采样不可微,再精妙的左右一致性损失都只能看不能用。很多初学者复现时最容易卡住的地方也在这里:拿grid_sample或自定义双线性采样时,必须确保采样网格坐标的单位、方向与视差定义一致,否则训练时看起来在下降,实际重建图早就歪了。
3. 三个损失函数一起工作:从“重建像”到“几何一致”
原文最大的贡献,是把无监督深度估计的训练目标拆成了三个互相咬合的损失:光度一致性损失、视差平滑损失、左右一致性损失。三者不是简单相加,而是各管一个层面的“不变量”。
3.1 光度一致性损失:先把重建图长成原图的样子
第一个损失是重投影后的图像和目标图像之间的外观匹配损失。最常见的组合是 L1 损失加 SSIM(结构相似性指数)。公式上大概长这样:
appearance_loss = 0.85 * (1 - SSIM(重建图, 目标图)) / 2 + 0.15 * L1(重建图, 目标图)为什么要两个一起用?L1 对颜色整体偏移敏感,但容易忽视局部纹理结构;SSIM 对亮度变化更鲁棒,更关注梯度结构,但单独用 SSIM 又容易出现颜色整体漂移。两个加权组合后,在光照变化和传感器噪声下要稳得多。论文把 SSIM 权重定在 0.85,这个数值我当时没在意,后来在不同数据集上调参才发现,0.85 左右确实是一个很难被推翻的甜点位——因为它恰好保持了边缘锐度,又不过度惩罚亮度偏差。
这个损失隐含的假设是:场景是朗伯体、左右相机光照一致、场景静止。满足这些条件的像素,重建误差越小,视差越可信。问题在于现实场景几乎处处违反这些假设:车窗反光、运动物体、边界遮挡。所以后面所有自监督深度模型的工作重心,几乎都压在“怎么处理这些违反假设的像素”上。
3.2 边缘感知平滑损失:把噪声梯度从深度图里清出去
如果只有光度损失,网络会在纹理稀疏的区域摆烂。想一想纯色墙壁、天空或者路面——从重建的角度看,这些区域任何视差值都能找到相似颜色,损失几乎给出零梯度。于是网络会随机输出剧烈的视差抖动,表现为深度图上密集的噪声斑块。
解决办法是给视差图加平滑损失:惩罚视差图在 x、y 方向的一阶梯度,强迫它尽量平缓。但直接全图拉平很笨,会把物体边缘的真实深度断裂也抹掉。所以论文给平滑损失加了一个用输入图像梯度做成的权重:
smooth_loss = |∂D/∂x| * exp(-|∂I/∂x|) + |∂D/∂y| * exp(-|∂I/∂y|)意思是:输入图像边缘处(梯度大),视差可以剧烈变化;输入图像平坦处(梯度小),视差必须平滑。这个设计很聪明,它把“深度边缘”和“图像边缘”做了弱关联,既不惩罚真实边界,又能清理平坦区域的噪声。
实操上有一个隐蔽细节:输入图像梯度的权重需要detach,也就是不要让这个权重本身参与梯度回传。原因是网络为了降低平滑损失,会反过来修改输入图像梯度的利用方式,导致图像纹理被错误抑制。这一步我在复现时漏掉过,结果训练中期路面出现了一大片纹理异常,排查了很久才定位到是梯度反传路径的问题。
3.3 左右一致性损失:两个视角互相咬合的最后一道锁
前两个损失加起来,其实已经能让网络输出一个看起来不错的视差图:重建得够像、区域够平滑。但只靠这两项,左右两套视差之间仍然可能是各自为政的。于是论文引入了第三个,也是本篇真正的主角——Left-Right Consistency Loss。
左右一致性的直觉是:如果左视差图在某个像素认为物体视差为d,那么把左图这个像素按d移动到右图对应位置后,右视差图在那个位置的值应该和d相差无几。换句话说,从左右两个方向看同一个三维点,看到的视差应该是同一个数。损失就是这两个视差之间差异的绝对值:
lr_loss = |D_left - D_right_flipped|其中D_right_flipped是把右视差图按左右方向做几何对齐后的结果。这个损失的威力在于,它是纯粹的几何一致性约束,不需要任何图外观信息。它强制网络在左右两个视角之间做出同一个空间结论,而不是各自“看起来合理”。
正是这个损失,让整张深度图变成一个互相牵制的整体:只要左视差和右视差有一处对不上,梯度就会同时修正两侧。这也是为什么该方法在许多无纹理区域和重复纹理区域,比单纯的光度法表现更稳——即便颜色上完全没有信息,左右几何关系仍然在监督。
我做一个表格把这三种损失的作用和弱点整理出来,读代码时对照着看会清晰很多:
| 损失 | 监督信号 | 主要作用 | 主要弱点 |
|---|---|---|---|
| 光度一致性 | 左右原图与重建图的相似度 | 让视差符合像素视角对应 | 遮挡、反射、动态物下失效 |
| 视差平滑 | 视差梯度 + 图像梯度权重 | 清理平坦区域噪声,保持边缘 | 细长结构容易被压平 |
| 左右一致性 | 左右视差互相映射的差异 | 保证两个视角几何自洽 | 遮挡区域被硬性要求一致 |
4. 那些论文里没有明说但会影响成败的训练细节
4.1 最大视差范围决定网络表达能力的上限
论文在输出视差时,不是让网络直接回归任意大数值,而是通过 sigmoid 激活把视差限制到一个最大范围。常见的设置是最大视差约为图像宽度的 0.1 倍:输入宽度为 512 时,最大视差大约 51 个像素。
这个细节非常关键。最大视差设得太小,近处大视差的物体(比如紧贴镜头的栏杆、前车车尾)会被硬性截断,深度图看起来发糊;设得太大,网络把大量表达能力浪费在极近处,远处小视差的区分度变差。如果按图像宽度比例来设,在不同分辨率之间迁移时,网络的经验可以直接平移。
我在实际训练里还有一个体会:最大视差应该结合数据集的“最近有效深度”来调。训练集中最近物体的距离决定了最大视差的实际值,如果你的任务里相机离物体最近可以到 30 厘米,而默认按 0.1 倍宽度设置,很可能把贴脸物体直接抹掉。这个数值值得作为超参数单独扫一遍。
4.2 超参数从哪里起步比较稳
网络结构之外,训练超参也很影响结果。论文以及后续大量复现使用的组合基本比较一致:Adam 优化器,初始学习率大约 3e-4,批次大小 8 到 12,输入分辨率 512x256,训练轮数在 40 到 50 个 epoch 之间。学习率在后半段做衰减。
损失权重方面,一个比较稳的起步配置是:
total_loss = appearance_loss + 0.1 * smooth_loss + 1.0 * lr_consistency_loss也就是说,光度损失和左右一致性损失为主,平滑损失的权重控制在 0.1 左右。我调过的任务里,平滑权重低于 0.05 时,深度图会出现细密噪声;高于 0.3 时,物体边缘会被越抹越平,尤其是细栏杆、树叶这些结构。左右一致性权重也不是越大越好,因为遮挡区域本来就是矛盾的,权重过高会逼着网络在遮挡边界做出扭曲的视差来强行满足约束。
4.3 边缘填充、遮挡区域和数据分布里藏着的坑
训练时重投影采样会遇到图像边缘越界。常见的三种处理方式:零填充、最近邻填充、镜像填充。零填充是最省事但最坑的——它会在图像边缘产生一排不存在的黑边,网络很快会学会用错误的视差来抑制这些黑边产生的误差,边缘深度会出现严重伪影。前后对比下来最稳的是镜像填充:它不引入真实世界中不存在的颜色,给边缘一个相对自然的延续。
遮挡区域则是左右一致性的天然痛点。前景物体左侧(或右侧)的像素,在另一个视角里根本没有对应点,左右一致性规定那里必须一致,这本身体现就是一个过度约束。早期实现通常不做遮挡掩码,所以边缘处会出现某种“妥协”——深度值被推到一个两边都不太满意、但误差可接受的中间值。后来的自监督方法(比如引入最小重投影损失、自动遮挡掩码的那一批)很大程度上就是为了修补这里。
数据分布同样要小心。用双目图像对训练的模型,会被训练数据里的相机配置“驯化”。如果你只用一种固定基线的双目数据,模型对近处和远处的尺度感受就会偏难;实测其他相机时,输出深度缺少度量意义。至少要保证训练集中的场景包含丰富的近距离物体和远距离远景,否则网络只在某个深度区间里“舒服”,区间之外全部胡猜。
5. 测试阶段还能再薅一把左右一致性的羊毛
5.1 左右一致性作为质量图:哪里能信,哪里不能信
这个方法有一个非常划算的隐藏福利:训练时算过的左右一致性,在推理时也能继续用来判断每个像素的可靠性。
测试阶段,如果手里仍然有左右图,可以分别拿到左视差图和右视差图,然后做一轮反向映射,计算每个像素的左右一致性误差。误差小的位置,说明两个视角对该点的深度结论一致,可信度高;误差大的位置,通常落在遮挡边界、反射区域或动态物体附近,可信度低。这张“可信度图”不需要任何额外网络,直接从网络的几何输出中算出来。
这个特性在很多落地场景里很实用。比如在边缘检测、点云滤波、立体匹配后处理里,可以用它把不可靠的深度点过滤掉,避免下游任务拿到一堆边缘毛刺。哪怕只做可视化,我也建议把左右一致性误差画出来,它比深度图本身更容易暴露模型在哪些局部犯傻。
5.2 测试期视差融合:不增加额外网络但提升明显
论文在推理时还做了一步后处理:利用左右一致性,把左视差图和右视差图的互补信息融合起来。因为遮挡区域里,左视角看不到的点在右视角可能能看到,两套视差各自的错误区域往往互补。于是可以做一个加权融合——左右一致性误差小的像素给高权重,误差大的给低权重,再把两套视差按权重合成一份最终视差。
这样做的收益很直接:边缘更连贯,遮挡边界的毛刺更少,整体平滑度提升。关键是这一步和训练完全无关,不需要重训网络,只需要在推理时多算一次重投影和一致性误差。我在测试中对比过融合前后的深度图,融合后的边缘抖动明显减少,尤其是在远处天际线和车辆侧面这些区域。
注意一个前提:要享受这个融合收益,推理时仍然需要同时输入左右两幅图。如果你的产品形态是纯单目相机、推理时只有一张图,那这部分后处理就用不上;但训练阶段学到的几何先验仍然保留在模型里,所以单图推理时深度图依然可看。
6. 这方法撑得起什么场景,又在哪些场景露怯
6.1 失效场景分析:不只是争议,而是几何先验的边界
没有一种方法是万能的,左右一致性非常依赖几个隐式假设,一旦场景突破这些假设,模型会迅速失去可靠性。
第一个软肋是动态物体。光度一致性损失要求“同一个三维点在左右视角中颜色一致”,但一辆正在行驶的车,在左右两个图像中的外观和位置本身就因为运动而不一致,重建误差完全不可信。左右一致性损失也管不住它,因为两个视角看到的根本不是一个静态世界。所以行人、车辆穿行的城市道路场景,早期这类方法的效果会明显下降。
第二个软肋是反射、透明和细长结构。车窗会倒映背景,重建出的图像可能因为反射完全对不上;细长的电线、栏杆在低分辨率下几乎没有足够的纹理锚点,平滑损失会把它们压成整片平滑的区域。第三个软肋是训练分布本身。换一个相机基线、换一个分辨率,深度尺度和表达能力都可能需要重新适配。理解这些边界,比背诵损失公式更重要——至少接手一个项目时,你能第一时间判断它适不适合用左右一致性的方案。
6.2 后来者往哪里改:从左右约束到多帧约束的演进线索
读这篇论文时,最好把它的位置放在演进脉络里看。它的核心贡献是把“左右几何自洽”变成了训练信号,后继工作基本沿着三条线扩展。
第一条线是把“左右两帧”扩展成“连续多帧”。既然左右两帧可以作为监督,那视频里前后多帧也可以——于是出现了用位姿网络估计相机运动、把相邻帧重投影到当前帧的系列方法。第二条线是处理动态物和遮挡。后来的最小重投影损失和自动遮挡掩码,本质上就是给光度一致性加了一个“只信误差最小的那个视角”的开关。第三条线是引入传统立体匹配的先验作为辅助信号,也就是深度提示的思路,把经典算法的计算结果和网络学习结合起来。
回看这些演进,最让我感慨的是,后来的工作改得很大,但核心框架基本没变:一种几何上合理的监督信号 + 可微重投影 + 一组处理边界的正则项。只要理解这篇论文的损失设计思路,读后续那些五花八门的无监督深度估计工作时会快很多。
6.3 我最后留在这篇论文空白处的几行批注
如果让我给这篇论文一个标签,我会写“无监督深度估计的分水岭”。它不一定是最早提出用重投影做自监督的,但却是把左右一致性做成完整训练框架、并带领一批后续工作转向几何自监督的代表性工作。
我个人复现和改造下来,最深的体会是:与其死记它用了哪些损失项,不如理解它为什么把这三项损失绑在一起。光度损失提供像素级外观证据,平滑损失提供结构先验,左右一致性提供几何互锁——三者在训练中互相补偿。很多后来失败的魔改,几乎都是把其中某一项权重拔得过高、或者为了工程简化扔掉了左右一致性。
我的建议也很简单:如果你打算把这个模型用到自己的项目里,不要一上来就追求 SOTA 指标。先在小数据集上把三个损失逐一打开,观察每个损失对最终重建图的影响。左右一致性方法的一大优点是它的训练曲线非常稳定——三个约束互相牵制,比单 loss 更容易收敛。这大概也是它能成为经典的重要原因。等你把三个损失如何协作的体感建立起来,后面的模型改进才有扎实的地基。