自监督单目深度估计是近年视觉领域的研究热点,它不需要昂贵的深度标注,只依赖相邻帧或双目图像之间的投影关系就能训练出不错的深度网络。但真正动手训练过的同学都知道,这套流程并不总是顺利:损失曲线上下震荡不下降、深度图呈现碎片化的乱纹、近距离物体深度估计严重失真,这些现象几乎都是收敛失败的信号。问题的核心在于,单纯的光度损失提供的监督信号非常弱,网络很容易陷入退化解,比如把所有像素的深度预测成同一个值,光度误差照样可以很低。要走出这个困境,就必须把光度损失和几何约束深度结合起来。

一、光度损失为什么容易让训练跑偏
光度损失的基本假设是:如果深度和位姿预测得足够准确,那么把源帧图像根据深度和位姿扭曲到目标帧视角后,应该和目标帧几乎一样。用公式表达就是对重投影误差做光度度量,常用L1误差加SSIM项的组合,再对所有像素求平均。这个设计看起来很合理,但它在数学上存在明显的病态性。
第一个问题是解空间太大。对于无纹理区域,比如白墙、天空,任何深度值经过投影后重投影结果都差不多,光度误差对这些区域的深度几乎不敏感,网络自然学不出有意义的结构。第二个问题是尺度歧义,单目设置下整体深度乘以一个常数、位姿除以同一个常数,投影结果完全不变,损失函数对此无能为力,模型输出只能确定相对尺度。第三个问题更隐蔽,网络可以学到一些作弊解,例如把深度预测成极端值,让大部分像素投影到图像外,从而巧妙地降低有效损失。这些退化解一旦形成,梯度就很难把参数拉回来,表现出来就是训练不收敛。
二、几何约束:给光度损失补上缺失的骨架
几何约束的思路是利用空间结构本身的规律,限制网络只能输出符合物理规律的深度图。最经典的是左右一致性约束:在双目设置中,用左图预测的深度生成右图的视差,再用右图预测的深度生成左图的视差,两者应当互为镜像。对这两个视差图施加一致性误差,可以有效惩罚深度图中的结构性错误,逼着网络输出平滑且边缘对齐的深度。
另一个重要的几何先验是深度平滑正则。自然的深度图在图像局部区域内变化缓慢,只在物体边界处突变。据此可以构造边缘感知的平滑项:对深度梯度乘以图像梯度衰减因子后再求绝对值之和。这样在平坦区域强制深度平滑,在纹理边界处则放宽约束,避免深度边缘被强行抹平。第三个几何工具是显式的投影几何约束,把相机内参矩阵引入采样网格的构造,让深度到像素坐标的映射严格遵循针孔模型,而不是让网络自由学习一个模糊的对应关系,这能显著减少尺度漂移。
三、组合损失函数的完整实现
把上述各项组合起来,一个工程上验证有效的损失函数如下面的代码所示。其中包含最小重投影策略(在多个源帧中选误差最小的一个,抑制遮挡区域的干扰)、自动掩码(过滤掉运动物体和静态像素)、边缘感知平滑项以及左右一致性项。
import torch
import torch.nn.functional as F
def compute_losses(target_img, source_imgs, depth, poses, K, lamb=0.85):
# 光度重建误差:L1 + SSIM
def photometric(pred, tgt):
alpha, beta = 0.85, 0.15
abs_diff = torch.abs(pred - tgt).mean(1, True)
ssim = SSIM(pred, tgt).mean(1, True)
return alpha * ssim + beta * abs_diff
warped_list, diff_list = [], []
for src, pose in zip(source_imgs, poses):
# 根据深度、位姿、内参做反向投影与前向投影得到采样网格
warped = warp_with_geometry(src, depth, pose, K)
warped_list.append(warped)
diff_list.append(photometric(warped, target_img))
# 最小重投影损失:遮挡区域取最小误差,避免错误惩罚
diffs = torch.cat(diff_list, 1)
min_diff, _ = torch.min(diffs, dim=1)
min_diff = min_diff.unsqueeze(1)
# 自动掩码:只保留重建优于恒等映射的像素
identity_diff = [photometric(src, target_img) for src in source_imgs]
identity_diff = torch.cat(identity_diff, 1)
identity = torch.min(identity_diff, dim=1, keepdim=True)[0]
reprojection_loss = min_diff + torch.clamp(min_diff - identity, min=0)
# 边缘感知平滑约束
grad_img = torch.abs(target_img[:, :, :, :-1] - target_img[:, :, :, 1:]).mean(1, True)
grad_depth = torch.abs(depth[:, :, :, :-1] - depth[:, :, :, 1:])
smooth_loss = grad_depth * torch.exp(-grad_img)
smooth_loss += grad_depth.transpose(2, 3) * torch.exp(-grad_img.transpose(2, 3))
smooth_loss = smooth_loss.mean()
total = lamb * reprojection_loss.mean() + (1 - lamb) * smooth_loss / 2
return total
这段代码里有几个细节值得强调。最小重投影策略解决了多帧训练中遮挡像素被错误惩罚的问题;自动掩码把那些用源图直接复制就能达到低误差的静态像素排除在外,防止网络偷懒;平滑项除以2是因为水平和垂直两个方向各算了一次。实践表明,这三个技巧中任何一个被去掉,训练稳定性都会明显下降。
四、训练稳定性的工程调优建议
损失函数之外,训练策略同样决定成败。首先是学习率,自监督深度估计对学习率非常敏感,建议从较小的值起步,配合余弦退火调度,一旦发现损失发散立即减半。其次是数据增强要克制,颜色抖动可以适度使用,但几何层面的翻转要慎重,垂直翻转会破坏相机位姿的物理含义,除非同步修正位姿标签。
多尺度输出结构也很关键。让网络在四个下采样尺度上分别输出深度,并在每个尺度上都计算损失,可以让低分辨率层先学到粗略的全局结构,再由高分辨率层细化边缘。但要注意上采样回原分辨率后再算光度损失,而不是在低分辨率图上直接算,否则小物体信息会被稀释。最后,建议训练初期冻结位姿网络的一部分参数,或者对位姿输出做尺度归一化,等深度网络输出稳定后再联合训练,这种分阶段策略能规避早期互相拉扯导致的震荡。把光度损失、几何约束和这些工程细节配合好,自监督深度估计的收敛问题基本都能得到妥善解决。