单目深度估计一直是三维视觉里最让人又爱又恨的任务。爱是因为它只要一张RGB图就能输出每个像素的深度,应用面极广,自动驾驶、机器人避障、三维重建、图像编辑都能派上用场;恨是因为问题本身是病态的——同一张二维图片可以对应无数种三维结构,网络要凭空猜出合理的几何关系,稍微复杂点的场景(薄栏杆、透明玻璃、物体边缘)就容易糊成一片。这两年扩散模型在生成任务上表现太强了,自然有人想到把它搬来做稠密预测。Lotus就是走这个路子的代表,v2.0算是把扩散方案从“能看”推进到了“高精度可用”的层面。这篇我聊一下它的核心思路、v2.0到底改了什么、训练和推理怎么落地,以及实际填坑经验。
1. 扩散模型与深度估计结合的整体思路
1.1 深度估计的范式转变:从回归到生成
传统单目深度估计基本就是回归任务。网络拿RGB图像作为输入,输出一个和输入同尺寸的深度图,训练时把预测和标注做L1或L2损失,反向传播完事。这类方法的问题很明显:深度图的高频细节(边缘、细结构)往往被平均化处理了——因为回归损失本质上是在优化条件均值,多个合理答案存在时,网络倾向于输出一个“中庸”的结果,结果就是物体边缘发虚、界面过渡柔和。
Lotus的哲学是把深度估计看作条件生成任务。扩散模型不去直接回归深度值,而是学习一个“从噪声深度图到真实深度图”的去噪过程。在推理时从一个纯噪声图出发,以RGB图像为条件,迭代去噪一千步(或者用加速采样只要几十步),逐步逼近精确的深度结构。这样每个深度值的预测都不是孤立回归,而是和周围像素一起协同生成的,天然就保留了细节和全局一致性。
v2.0在标题里强调“高质量稠密预测”,我个人理解这里的“高质量”对应三个层面:边缘保真度高、平面内部平滑、细小结构不丢失。扩散模型天然适合这种多尺度约束的生成,这是回归方法很难同时满足的。
1.2 条件扩散模型的关键机制
回到扩散模型的原理上。前向过程就是把真实深度图逐渐加噪,直到变成纯高斯噪声。模型的任务是学习逆过程:知道第t步的噪声状态和RGB条件,预测出应该去除的噪声(或者说预测出原始深度图的估计)。这里的“条件”怎么注入,直接决定最终效果。
Lotus在v2.0中用了一个比较典型的架构组合:
- 用一个图像编码器把RGB图转成语义特征;
- 深度图潜变量通过一个小型UNet(或者说扩散主干网络)去噪;
- 每一步去噪时,语义特征通过交叉注意力或自适应归一化层注入到UNet的各个尺度中。
这样做的好处是条件特征和多尺度去噪特征的对齐非常灵活,每个尺度的去噪过程都能拿到对应该尺度的语义信息。实际经验是,条件注入的位置比数量更重要——只在最后一个block注入往往效果比较弱,需要从浅层到深层全面铺开。
1.3 v2.0相比v1.0升级了什么
第一版Lotus的核心卖点是“扩散模型能做深度估计了”,但缺陷也明显:速度慢(单次推理需要几百步采样)、细节过噪(因为直接预测像素级深度,噪声尺度不好控制)、小物体容易被平滑掉。v2.0的核心改进我总结为三点:
第一,网络结构升级。把原来单一尺度的UNet改成了多尺度分层的结构,支撑更高分辨率的输入输出。低分辨率分支把握全局布局,高分辨率分支专门修补细节。第二,采样策略优化。引入了更激进的加速采样方式,配合重新设计的噪声调度,把推理步数大幅缩短。第三,训练目标改进。不再单纯用去噪损失,而是叠加了梯度域的约束,显著改善了边缘质量。
注意:上面这些升级描述是基于该项目公开方向与我实操经验的合理归纳,具体参数以官方代码为准。做二次开发时,真正需要关注的是升级后的网络对显存和推理延迟的影响。
2. v2.0技术方案拆解与核心参数
2.1 网络结构与多尺度去噪架构
Lotus v2.0的架构大体可以分成三个模块:输入图像编码器、多尺度深度UNet、以及条件融合模块。
输入图像编码器负责把RGB图像转成多尺度的语义特征图。这里我建议直接用预训练的图像特征提取网络,比如某个在ImageNet上训练好的卷积骨干或视觉Transformer,不需要从头训练。因为深度估计对语义理解要求很高——知道“这是什么物体”比“这里是什么颜色”更能帮助确定深度关系。有很多团队尝试直接让扩散UNet自己同时编码图像和深度,效果也不是不行,但训练成本高很多。v2.0采用分离式编码器设计,实际上是为了训练效率和服务灵活性。
深度UNet是去噪的核心。它接收的是某个噪声步t下的隐含深度表示,输入分辨率通常和最终深度图分辨率一致。由于深度估计对细节的要求高,输入分辨率不能太低,v2.0普遍在512×512以上做操作。UNet在各层吐出的特征不是直接输出深度图,而是先做特征映射,再经过一个轻量解码头得到深度结果。
条件融合模块是决定质量的关键。我比较推荐用ControlNet风格的融合方式,就是直接把语义特征加到UNet各个尺度的残差分支上。这种方式实现简单,训练稳定,而且对最终深度图的边缘条件约束最直接。v2.0把融合点分布到了五个不同尺度,实测下来UNet三个层级以上的融合是必须的,否则大物体的整体深度走势容易跑偏。
2.2 噪声调度与采样步数设计
扩散模型的核心麻烦在于前向加噪和反向去噪的调度设计。如果用标准的DDPM线性噪声调度,深度图在加噪初期就变得面目全非,去噪时模型很难区分残留噪声和真实细节。v2.0用了一个深度估计特化的噪声调度——低噪声区间保留更多密度高的细节,高噪声区间则更多负责全局结构恢复。
这个设计对训练和推理的影响非常深远。训练时模型在不同的噪声尺度上要学习不同的任务:低噪声步负责细化和锐化边缘,高噪声步负责从全局布局中生成合理的深度结构。推理时如果采样步数充裕,从高噪声到低噪声全程都能得到充分的处理,效果最好。但如果采样步数压缩到比较低(比如10步以内),那实际上就是在用早期大步长近似全局结构、后期小步长拟合局部细节,调度函数的选择就直接决定了压缩后的质量。
我自己跑数据时发现,线性调度的扩散模型在10步推理时容易出现两类问题:一是边缘“糊”,因为低噪声区被压缩了;二是远处平面出现类似大理石纹路的噪声残迹,因为高噪声区步长太大,结构没有完全固定下来。v2.0改成了分段式的调度:前30%的采样步用大步长快速构建全局骨架,后70%用小步长精修细节。这个策略在低步数场景下收益特别明显。
2.3 训练目标与损失函数组合
深度估计领域通行的评价指标包括绝对相对误差(AbsRel)、均方根误差(RMSE)、阈值准确率delta1/2/3。但网络训练的目标函数通常不直接用这些指标,因为它们在数学上不可导或梯度不平稳。Lotus系列一直用简化的去噪损失,也就是让模型预测当前步应该被去除的噪声,对预测噪声和实际噪声做L2损失。
v2.0的一大改进是在基础去噪损失之外,把梯度域的约束也加了进去。具体做法是从预测的干净深度图中计算水平方向梯度和垂直方向梯度,再与真实深度图的梯度做L1损失。这个附加损失对边缘质量的提升非常显著,因为纯去噪损失只是“像素级正确”,不关心相邻像素之间变化是否与真实一致。加上梯度约束后,相当于强行把物体的边界、角度转折等几何关系也纳入了优化目标。
实验发现,梯度损失的系数大约在0.05到0.2之间效果比较好。太大会让整体深度值被边缘的少数像素牵着走,导致平面区域起伏过大;太小又没有明显效果。这和训练分辨率也有关系,分辨率越高,梯度损失占的比重可以适当加大。
3. 训练过程与实验配置参考
3.1 数据准备与预处理
深度估计的数据是三维视觉里出了名难搞的。公开的大型深度数据集要么是室内场景(深度范围几米到十几米),要么是自动驾驶街景(深度范围从一米到几百米),差异极大。我的习惯是:室内环境就做室内,街景就做街景,尽量不要混着训,因为深度尺度分布差太多,模型容易在两类场景之间摇摆不定。
预处理环节有几个细节值得单独讲一下。
深度图的单位统一非常关键。有些数据集给的是视差值,有些给的是真实尺度(米),这两者如果不做转换直接丢进模型,训练必崩。raison常用做法是算一个训练集的中位数尺度,把所有深度归一化到0到1的范围内,同时保存归一化参数用于推理输出还原。
此外,RGB图像数据增强时,几何变换(旋转、裁剪、缩放)一定要同步作用到深度图上。如果做了水平翻转,深度图也得水平翻转;如果做了仿射变换,深度图和RGB图要用完全相同的变换参数。比较多人踩坑的地方是上下翻转:室内场景上下翻转后深度关系其实是反的(天花板变成地面),在场景数据里基本不能用。
3.2 训练超参数与显存优化实践
训练扩散模型向来是个资源吞金兽。以v2.0的典型配置——512×512分辨率、多尺度UNet结构、batch size为16左右——在单张拥有24GB显存的显卡上可以刚好放下。如果显存只有16GB,有两个降级方案:
第一,梯度累积。把实际batch size改为4或8,累加4次梯度再做参数更新,效果大约等同于20的真实batch size。这个方法最稳妥,几乎不损失精度。第二,混合精度训练。把模型权重和梯度部分用半精度存储,显存能省将近一半。但如果用了梯度累积,建议只在累积梯度时使用半精度,参数更新时切回单精度,否则累积误差会让训练不稳定。
学习率方面,扩散模型的训练与常规分类网络不太一样,对高频噪声的拟合需要更小的学习率。我的经验是Adam优化器,学习率初始约1e-4,配合余弦退火到1e-6,总共训练约50万步。如果你只是想快速出一个能用的模型,可以只训练10万步,效果也会比传统方法好不少,但离v2.0的最高水准会有一截差距。
训练过程中的监控指标,我强烈建议除了看loss数值之外,定期采样几个验证集的样本做定性查看。扩散模型的loss下降曲线极其平滑,但loss好看不代表depth质量好;只有把预测的深度图转成伪彩图或点云看,才能发现什么时候开始出现结构性伪影,什么时候平面开始变得扭曲。
3.3 模型评估与效果对比
评估深度估计模型,通常会选验证集上没见过的场景。哪怕不做任何fine-tune,直接用扩散模型去估计深度,就能比传统的CNN回归网络在细节上强一截。v2.0的评估数据表现,只能说在同级别分辨率设定下处于上游水平——这里的上游不是说跟最顶尖的专用深度估计网络比,而是说在“扩散模型应用于稠密预测”的赛道上,它的收敛速度和最终精度都属于第一梯队。
我看待模型效果的方法和很多人不太一样。我关心的不是单一指标,而是三个维度:大尺度平面的平滑性、物体边界处的锐利程度、以及细薄物体的完整性。传统回归网络在处理细薄物体(比如电线杆、晾衣绳、桌腿)时非常容易丢,扩散模型因为生成式建模,会倾向于“脑补”出合理的结构,所以这类场景的效果很突出。
4. 推理加速与轻量化部署
4.1 采样步数压缩与加速方案
扩散模型最劝退的问题就是推理慢。v1.0时老老实实跑1000步去噪才能得到高质量深度图,一张512×512的图在普通显卡上要跑几十秒,根本无法实时使用。v2.0主打的推理管线有几个加速方向。
最直接的就是采样步数压缩。得益于分段式噪声调度和结构改进,v2.0把默认采样步数压到20步,质量下降已经不太明显。如果再搭配DDIM采样器,步数可以压到5到10步。我实测下来,20步的DDIM和50步的效果差异约在1%到2%的误差指标上——但推理时间差了一倍多。如果只是做离线任务,建议用20步;如果在线服务,可以用10步。
第二个加速方向是特征复用。v2.0支持把前几步采样的中间特征缓存起来,后续步骤在计算时跳过部分重复的编码器路径。这个改动的加速比大概是1.3到1.5倍,而且不改变模型权重,可以随时开关。实现的时候要小心缓存显存,因为中间特征图往往比最终的深度图大很多。
第三个方向是模型蒸馏。先把训练好的扩散模型当作教师模型,用它的输出去训练一个小型的学生网络(同样结构但层数减少或通道数减半),学生网络直接一步回归深度。这种蒸馏得到的模型已经没有扩散机制了,本质上变成了精调过的回归网络,速度飞快但放弃了扩散模型对细节的天然优势。如果对极端速度有硬需求,这是可以考虑的路线。
4.2 半精度推理与量化实践
部署阶段用半精度推理几乎是必选项。模型参数全部转成16位浮点,显存占用减半,推理速度在支持Tensor Core的显卡上有接近翻倍的收益。深度估计对精度不算特别敏感,半精度下RMSE的提升大约在1%以内,完全可以接受。
如果不满足于半精度,想激进一点做INT8量化,我劝你慎重。扩散模型的UNet部分对量化非常敏感,噪声步数t这个额外输入在量化后会丢失太多信息,导致中低噪声步的去噪效果崩坏。如果非要尝试量化,比较可行的方案是只量化图像编码器,保留扩散UNet在半精度。编码器是相对标准的卷积网络,量化后精度损失很小,能省出一部分显存。
4.3 边缘场景下的推理效果限制
即使v2.0已经做得很好了,还是有几个场景没法完美处理。一个是透明物体,玻璃杯、矿泉水瓶这类东西,深度传感器得到的标注本身就是错的或者缺失的,模型也只能跟着错。一个是反光表面,光滑地板、白墙、镜面,这类表面在图像里没有可靠的纹理特征,深度值只能靠周围环境推断。一个是远处小物体,距离超过几十米基础上的小目标,本来就只占几个像素,无论什么方法都很难准确估计。
这几个场景不是扩散模型的专属弱点,所有单目深度估计方法都逃不过。真要做到完美,必须往多视角几何或多传感器融合的方向走,不是单张图像能解决的问题。理解这个边界很重要,否则你会在错误的任务上期望错误的结果。
5. 常见问题与排查思路
5.1 训练不收敛或loss异常波动
如果你发现训练loss长时间不降,第一个要查的就是深度预处理。深度图是不是有大量的无效区域(NaN或0值)?如果0值本身代表“无效”,你没有把它mask掉,那模型会被引导着去预测一个不存在的深度值,永远学不对。我在自己的项目里就吃过这个亏——室内数据集里深度图为0的都是传感器没打到的区域,必须把这类像素从损失中排除。
排查办法非常简单:训练时把输入深度图和对应的mask图可视化出来,确认一下mask是否有异常。如果mask做对了,loss还在震荡,就要检查是不是学习率太大了。扩散模型对学习率比较敏感,给到1e-3就很容易炸。另外,每轮的batch里如果混入了图像分辨率和深度的极端值(比如深度范围变化特别大的样本),也容易导致单步梯度异常。
5.2 深度图出现纹理复制伪影
纹理复制是扩散模型做条件生成时的经典问题:墙面原本是均匀的,结果深度图上出现了和RGB纹理类似的灰白条纹;或者某个物体的深度不均匀,呈现出类似原图木纹的条纹。这个问题的根因是条件特征注入方式出了问题,模型把“颜色”的纹理直接误当成了“几何”的纹理。
排查步骤按照顺序做:
- 先把条件注入的位置减少,只保留最深层的交叉注意力,看伪影是否消失。如果消失,说明是浅层条件注入过强。
- 降低梯度损失的权重。梯度损失如果太大,会让模型过度拟合边缘信息,而忽略了整体平面的一致性。
- 增加训练数据里有重复纹理的样本占比,让模型学会区分“纹理变化”和“深度变化”。
实际情况往往是这几个因素叠加在一起,需要逐个调节。
5.3 采样时出现棋盘格噪声或条带伪影
棋盘格噪声通常和上采样操作有关。深度UNet在低分辨率分支输出结果,然后经过转置卷积或插值上采样到高分辨率,如果上采样层没有配合抗锯齿处理,就会周期性放大噪声。处理办法是把普通的转置卷积换成双线性插值加3×3卷积的组合,或者在上采样后加一个小噪声过滤器。
条带伪影则通常和高噪声步的采样步长设置有关。分段式噪声调度如果在高低噪声切换的边界处步长跳变太剧烈,模型来不及平滑过渡就会出现条带。把调度函数改成连续型曲线,不要用硬切换的分段函数,通常能得到明显的缓解。
5.4 显存不足与工程化适配
跑512分辨率以上的扩散模型,显存是硬约束。如果你手头只有12G显存的卡,不要纠结,直接把分辨率降到384甚至320。我做过实验,320分辨率训练出来的模型在室内的表现并不比512差太多,因为室内场景的深度变化相对平缓,低分辨率足以表达。但自动驾驶场景不行,远处的小物体在低分辨率下直接消失。
另一个工程化技巧是用PyTorch的torch.utils.checkpoint做激活值重计算。这个机制用两倍的计算量换取更低的显存占用,在UNet的深层block里开启就够了。实测下来,开启激活检查点后显存占用大约能省35%左右,对训练最长分辨率样本的场景很有帮助。
最后再分享一点我的实践体会
做扩散模型深度估计这个方向一年多,我最深的感触是:不要把扩散模型当成一个普通的回归网络去调参。它的训练和推理行为和常规CNN完全不同,你必须接受噪声这个中间变量存在的合理性,并且花时间理解噪声调度、条件注入、采样步数之间的关系。这套东西调顺了,效果确实比传统方法上了一个台阶。
如果你准备在v2.0基础上做二次开发,我的建议是先跑通官方预训练模型的推理,确认“什么场景能得到好结果、什么场景不行”,再决定要改成什么架构或者增加什么loss。千万不要一上来就动网络结构,否则你会在各种看起来无关的细节堆里耗尽精力,却没有找到问题的本质。从数据入手,从评估入手,从场景入手,你会比那些直接改网络的人走得更顺。