做过点云重建或者单目深度估计的读者多半遇到过这样的场景:网络输出的法线图整体看起来没什么问题,可一旦放大到局部细节,就会发现相邻像素或相邻点的法线朝向彼此矛盾,这一块朝外,那一块朝内,渲染出来的表面坑坑洼洼。更深层次的麻烦在于,当我们同时加上法线一致性损失和法线平滑约束时,两个损失项之间往往会互相拉扯,训练曲线震荡不止。这篇文章就来拆解这两个约束的本质,分析它们为什么冲突,以及工程上有哪些成熟的调和手段。

先搞清楚两个约束各自在管什么
法线一致性损失的目标是让预测法线与某个参考方向对齐。这个参考方向可以是真值法线图,也可以是由深度图梯度推导出的伪法线,还可以是点云局部平面的拟合方向。以深度图场景为例,设深度为 D(u, v),相机内参已知,那么表面法线可以由两个切向量的叉积得到,预测法线 n 与该参考法线 r 之间的损失通常写成 1 减去两者点积的绝对值或直接用点积本身:
import torch
def normal_consistency_loss(pred_n, ref_n, use_abs=False):
# pred_n, ref_n: [B, 3, H, W],均已单位化
dot = torch.sum(pred_n * ref_n, dim=1)
if use_abs:
# 对朝向不敏感,只约束方向所在的直线
return (1 - dot.abs()).mean()
else:
# 严格约束朝向,内翻外翻都算错
return (1 - dot).mean()
上面代码里 use_abs 这个开关很关键。不用绝对值时,损失会惩罚朝向翻转,这正是我们处理法线朝向混乱时想要的;但参考法线本身不可靠时,绝对值版本更稳妥。一致性损失本质上是一个逐点约束,它只看单个位置上预测和参考是否对齐,完全不管邻域。
法线平滑约束则管的是空间连续性。它假设真实物体的表面在局部是光滑的,相邻位置的法线方向不应该突变。常见的实现有两种:一种是对法线图做空间梯度惩罚,惩罚水平垂直方向的差分;另一种是计算相邻法线之间的夹角余弦,惩罚小余弦值。前者计算便宜,后者对旋转更鲁棒:
def normal_smooth_loss(pred_n, weight_tv=1.0, weight_ang=1.0):
# TV 形式:惩罚相邻像素法线的差分
dx = pred_n[:, :, :, 1:] - pred_n[:, :, :, :-1]
dy = pred_n[:, :, 1:, :] - pred_n[:, :, :-1, :]
tv = dx.abs().mean() + dy.abs().mean()
# 角度形式:惩罚相邻法线点积过低
dot_x = torch.sum(pred_n[:, :, :, 1:] * pred_n[:, :, :, :-1], dim=1)
dot_y = torch.sum(pred_n[:, :, 1:, :] * pred_n[:, :, :-1, :], dim=1)
ang = (1 - dot_x).mean() + (1 - dot_y).mean()
return weight_tv * tv + weight_ang * ang
两个约束一个对齐外部参考,一个维护内部秩序,听起来各司其职,但在真实表面上,特别是边缘、褶皱、薄结构这些区域,光滑假设本身就不成立,冲突便从这里滋生。
冲突是怎么发生的
第一类冲突来自参考信号与几何事实的矛盾。假设参考法线由深度图差分推导,在深度不连续的边缘处,差分算出来的所谓法线其实是两个不同表面的混合体,方向可能严重失真。此时一致性损失拼命把预测法线往一个错误方向拉,而平滑损失又要求它与两侧的真实法线保持连续,梯度在边缘附近反复打架,训练自然震荡。
第二类冲突更隐蔽,出现在低置信度区域。物体表面朝向接近垂直于视线的区域,深度图梯度数值极大,微小的深度噪声会被放大成剧烈的法线偏移。一致性损失在这些区域的监督信号几乎是噪声,而平滑损失会试图把噪声抹平,两者给出的梯度方向频繁相反。如果简单地把两个损失加权相加,等价于让网络在矛盾指令之间取折中,结果往往是两边都不讨好:法线既没有对齐,也没有真正平滑,只是收敛到了一个含糊的中间状态。
第三类是全局朝向的歧义问题。仅靠逐点一致性损失,网络可能在不同连通区域学出不同的内外朝向,因为每一块局部看都是对的。平滑约束理论上能传播朝向,但它的梯度在大角度差异时几乎消失,传播能力有限。这就解释了为什么有些模型训练损失很低,重建出来却仍有大块区域内外翻转。
工程上的三种调和手段
第一种思路是损失加权调度。训练早期让平滑损失占主导,先把法线场整理成一个大致连续的骨架,再逐步提高一致性损失的权重,做精细对齐。这样避免了网络一开始就追逐带噪声的参考信号。实现上可以按训练轮数做余弦调度,也可以根据参考法线的置信度动态加权,比如用深度梯度模长反比作为置信度,梯度大的边缘处降低一致性损失权重:
def weighted_consistency_loss(pred_n, ref_n, depth):
# 由深度梯度模长估计置信度,边缘处降低权重
gy = depth[:, :, 1:, :] - depth[:, :, :-1, :]
gx = depth[:, :, :, 1:] - depth[:, :, :, :-1]
conf = 1.0 / (1.0 + 50.0 * (gx.abs() + gy.abs()))
conf = torch.nn.functional.interpolate(
conf.unsqueeze(1), size=pred_n.shape[2:], mode='bilinear')
dot = torch.sum(pred_n * ref_n, dim=1, keepdim=True)
return (conf * (1 - dot)).mean()
第二种思路是换用鲁棒损失函数。把硬性的点积损失换成 Charbonnier 或 Cauchy 鲁棒核,让大误差区域的梯度被自动压制,等效于告诉网络:参考法线明显不对的地方,别硬拟合。平滑损失同理,可以只在法线夹角小于某个阈值时才施加惩罚,物体真实的折痕边缘自然被排除在外。这种做法实现简单,几乎不增加计算量,是性价比最高的方案。
第三种思路是引入几何先验做全局朝向传播。先在点云上构建邻接图,用最小生成树或者传播算法从高置信度种子点出发,把朝向一致性逐区域确定下来,再将其作为软监督回填给网络。这相当于把逐点约束升级成了带连通性信息的全局约束,能有效解决分块翻转问题。代价是需要额外的图构建步骤,推理流程稍复杂,但对最终重建质量提升明显。
综合来看,法线约束的冲突并非不可调和,关键在于承认不同约束在不同区域的可靠性不同。边缘处信平滑、平坦处信一致性、朝向歧义靠全局传播,把这套组合拳打顺了,法线场的质量会有肉眼可见的改善,重建表面也就自然干净了。