光流估计是我接触深度学习之后觉得非常“拧巴”的一个任务。输入是两帧相邻图像输出的是一个和原图同样大小的二维矢量场每个像素点告诉你“往哪去了、动了多少”。听起来很直观等你真的上手调一次模型就会明白遮挡、运动边界、大位移、光照突变这些坑几乎每一个都能让你调破防。这篇文章想把光流估计从原理选型到训练实操的完整流程捋一遍顺便把我踩过的坑、试过的方案、以及最后稳定跑通的配置都记录下来。适合两类人看一是刚接触深度学习视觉任务、打算拿光流练手的开发者二是已经跑通过分类、检测但想在运动估计方向落地的朋友。深度学习方法之所以能在光流任务上打出一片天地核心在于它把“匹配相似像素”这件事变成了“网络自己学特征”。传统方法需要人工设计像素相似度度量、平滑约束、多尺度搜索策略而且对纹理缺失、遮挡区域容易失效。深度学习把这些问题交给大量数据和损失函数去解决模型自己决定什么样的特征适合匹配、什么样的运动模式需要保持边界。这篇内容不会只讲概念而是把数据准备、网络架构选择、损失函数实现、训练调参、常见坑位都放出来尽量做到看完能直接复现。1. 光流问题本质不是“找相似点”那么简单1.1 光流到底是什么光流的数学定义很干净。给定第t帧图像I₁(x,y)和第t1帧图像I₂(x,y)光流场F是一个和图像尺寸一致的二维映射F(x,y) (u, v)表示像素(x,y)在下一帧中移动到了(xu, yv)。理想情况下同一物理点的像素亮度在两帧之间保持不变这个假设叫亮度恒常性。这个假设在室内静态场景、相机平移的场景下基本成立但一到真实世界就漏洞百出。比如一辆车开过车前灯把路面照亮了同一个地面像素在两帧之间的亮度完全变了再比如一个人转身背后一半身体被遮住这些像素根本没有对应的匹配点。光流任务真正的难点不是“算出运动”而是“在没有可靠匹配信息的情况下依然能给出合理的运动估计”。我们做深度学习光流本质上就是在拟合一个极其复杂的非线性映射从“两帧图像的像素关系”到“稠密运动场”。网络需要同时理解纹理信息、边界信息、语义信息甚至要推理出遮挡区域背后的大致运动趋势。1.2 传统方法为什么吃力传统光流方法的代表是“亮度恒常 平滑约束”的能量最小化框架。思路是在亮度恒常的假设下找让匹配误差最小的位移场同时要求相邻像素的位移尽量平滑。这套框架在数学上很优雅但有两个致命弱点。第一能量函数是非凸的直接优化很容易掉进局部最优。于是各种金字塔策略被引入先在小尺度上算粗略位移再逐层细化。这个过程对参数非常敏感不同场景可能要调不同层数、不同迭代次数泛化性很差。第二手工设计的特征对光照变化、遮挡、大位移几乎无能为力。纹理稀疏的区域比如白墙、天空匹配信息几乎为零传统方法只能靠平滑项硬拉结果就像糊了一层浆糊——边界被磨平了小物体直接消失。原因在于平滑约束是全局均匀作用的它不知道怎么在物体边缘“刹车”。1.3 深度学习为什么能打深度学习光流网络的核心优势在于“特征是学出来的”。网络从数据里学到了什么样的像素特征更鲁棒——比如它可能学到边缘、角点、甚至语义级别的特征来辅助匹配这远远超出了手工设计特征的能力范围。另一个容易被忽视的优势是“上下文信息”。传统方法在计算某个像素的光流时只考虑局部窗口内的信息而深度学习网络通过下采样扩大感受野能够看到整个物体的轮廓、甚至整张图的语义布局。举个例子一个行人在画面里被电线杆短暂遮挡传统方法会在这里直接断裂但网络如果之前学过“行人是一个整体、腿部运动是有规律的”它就能根据上下文把遮挡部分的光流大概率猜出来。这种跨区域推理能力是深度学习方法最值钱的地方。2. 训练范式选择监督、自监督还是无监督2.1 三种范式对比光流模型的训练方式和别的视觉任务不太一样核心问题在于“真值从哪来”。目前主流的路线有三条全监督训练、自监督训练、无监督训练。我一开始直接选了全监督后来才发现实际场景中真值几乎拿不到才认真研究起另外两条路。训练范式真值来源适用场景核心代价全监督合成数据或仿真器生成学术研究、预训练合成与真实场景有域差异自监督通过图像重建作为监督信号真实场景、无真值数据需要遮挡处理否则亮度恒常假设崩无监督循环一致性平滑约束视频序贯数据收敛慢小物体容易被平滑掉全监督的优势是训练稳定、指标漂亮因为loss直接优化真值和预测之间的误差。问题是真实世界几乎不可能大规模获取稠密光流真值除非用昂贵的动态捕捉设备或者人工标注根本不可行。我自己试过用合成数据预训练、再用少量真实数据微调效果不错但域差异还是很明显。自监督方案利用的是亮度恒常性假设预测的F把I₂ warp回I₁和原图I₁作重建误差。这个方案实现简单但问题也很突出——遮挡区域不满足恒常性旋转、光照变化也会让重建误差失真。所以真正的自监督方案必须加一个遮挡感知机制网络同时学一个“置信度权重”告诉loss哪些像素是可信的。无监督方案更像是把两种loss堆在一起光度一致性加平滑约束再叠一些鲁棒性设计。好处是彻底不依赖真值坏处是训练初期非常难收敛需要精心调节平滑损失的权重否则模型会偷懒——直接把整张图的光流预测成0因为这样平滑损失最小。2.2 监督信号合成数据为什么是主力全监督路线里合成数据几乎是唯一的选择。有人可能会问用视频序列配合传统算法生成伪真值行不行说实话行但效果很差。传统算法在弱纹理区域本身就不可靠生成的伪真值会把误差当作学习目标模型学到的全是传统算法的错误模式学术界把这个现象叫“自证偏差”。合成数据的正确做法是用渲染引擎随机生成三维场景摄像头在场景里运动这样你可以精确知道每个像素的三维坐标变化从而算出完美稠密光流真值。我实际使用下来的经验是合成场景需要包含足够多的随机性——随机纹理、随机光照方向、随机遮挡物、随机物体数量。不要用太“干净”的场景否则训练出的模型根本扛不住真实世界的杂乱。我在早期训练时犯过一个错误用了一个物体少、纹理规则的数据集结果模型在真实场景上一测试弱纹理区域直接放飞自我光流像噪点一样乱跳。合成数据的另一个坑是“分布单一”。如果合成的都是匀速平移场景模型对加速度、旋转、形变就会非常敏感。常规做法是对合成数据的运动模式做多样化约束比如加入二次曲线轨迹、随机旋转中心、随机物体形变这些细节对最终泛化能力的影响远大于网络结构本身。2.3 自监督的损失设计不能只有重建误差自监督做光流最关键的就是“遮挡感知”和“鲁棒损失”。先说说为什么不能只用重建误差。想象一个旋转的杯子杯子正面有纹理背面没有旋转过程中背面转到了正面位置这个像素点在两帧之间根本没有可匹配的信息。如果强制要求重建误差最小模型只能给出一个平均糊弄的位移这会让边缘区域和遮挡区域的估计明显变差。解决办法是让网络额外预测一个遮挡置信度图每个像素对应一个0到1的权重loss在低置信度位置自动降权。鲁棒损失也很重要。常用的方式是使用带阈值的“鲁棒Charbonnier损失”而不是纯粹L2损失。L2损失对离群点——比如光照突变产生的巨大重建误差——极其敏感一个错误像素就能把整张图的梯度带偏。鲁棒损失计算时近似于L1对大误差给一个封顶的梯度训练过程会稳定非常多。我实测下来同样的网络结构用鲁棒损失替代L2后收敛速度和最终精度都有明显提升。平滑损失也不能少。自监督方案如果只有重建损失模型可以在光照不变区域学得很好但在纹理稀疏区域会彻底放飞。平滑损失要求相邻像素的光流矢量尽量相似相当于是传统方法里的正则项。但它有个副作用会对物体边界产生过平滑。常规解法是设计一个“基于图像边缘的权重”如果当前像素正好在图像边缘平滑惩罚降低允许光流在边界处产生跳变。3. 数据准备与预处理决定成败的细节3.1 数据集的预处理流程光流模型的训练数据本质上是一个“帧对”序列每对图像要同步提供对应的光流真值和遮挡掩码。预处理流程比普通图像分类复杂得多我整理了一个自己常用的流程第一步图像尺寸统一化。因为网络需要固定尺寸输入训练前需要把两帧图像和光流真值同时做相同的缩放。这里有个容易忽视的点光流真值缩放后矢量值必须乘以缩放系数否则位移量就错了。比如图像从640缩到320光流值也应该除以2。我第一次处理时忘了这一步训练出来的模型位移量整体偏小排查了很久才发现是数据预处理的问题。第二步数据增强要同步作用于图像对和光流场。光流任务的数据增强比物体检测更麻烦因为图像几何变换会同时改变光流值。常规的随机翻转、随机裁剪、随机旋转都要仔细写转换逻辑确保图像变换公式和光流变换公式保持一致。以水平翻转为例图像要水平翻转光流的水平分量u要取反垂直分量v保持不变同时光流场的坐标轴也要对应翻转。第三步验证集和训练集要严格分开。这里指的是不同来源的数据而不是同一份数据切分。光流模型的泛化能力很依赖训练场景多样性如果训练集和验证集来自同一个渲染器、甚至同一批场景文件验证指标会虚高真实部署时立刻现原形。3.2 数据增强合成数据泛化到真实数据的关键很多朋友训练光流模型只做简单的随机裁剪和翻转然后发现模型在真实场景上性能平平。我踩过这个坑后专门研究对比了一轮增强策略总结下来最有效的是这几项颜色扰动。随机调整亮度、对比度、饱和度、色相。因为真实场景的光照变化远复杂于合成数据的固定光照颜色扰动相当于强迫网络学习颜色不变性。注意扰动幅度可以大胆一些增强过强也比过弱好。遮挡模拟。真实场景大量存在遮挡而合成数据的遮挡往往比较“干净”。我常用“随机黑色矩形块叠加”的方式模拟局部遮挡叠加位置随机同时把这个区域的遮挡掩码置为无效。这样网络能提前学会处理遮挡区域的模糊性。随机运动模式增强。在训练过程中对图像施加随机位移、旋转、缩放生成“合成运动”同时推导对应的光流真值。这相当于是免费的额外监督数据对提升大位移运动能力特别有效。我强烈建议把空间尺度变化类增强放在最后一步做也就是先做完颜色和遮罩增强再进行随机裁剪。因为裁剪会改变图像尺寸后续的颜色调整如果在裁剪后做边界上的颜色变化会干扰边缘特征。3.3 Batch Size与训练参数的实测数据我最初训练直接用Batch Size 8结果发现梯度噪声很大Loss曲线像心电图。后来把Batch Size提到16情况明显好转。原因在于光流任务的监督信号密度很高——每个像素都是一个监督点Batch Size太小会导致梯度估计方差大尤其在使用平滑损失的时候剧烈波动的梯度会让网络陷入震荡。学习率方面我的经验是初始学习率设在10的负四次方这个量级配合每5个epoch衰减一次的调度策略。如果发现Loss下降过慢不要急着调大学习率先确认数据增强是否太强、损失权重是否失衡。很多时候模型不收敛不是学习率问题而是某个loss权重过大把其他loss的梯度信号淹没了。我自己在一次实验中把平滑损失权重调高了10倍结果模型输出光流场几乎均匀——损失函数被“平滑项”主导网络发现均匀场就能拿到很低的loss就没必要学真的运动了。4. 网络架构选型从端到端回归到迭代式优化4.1 早期端到端网络直接回归光流场光流深度学习的早期方案很直接把两帧图像在通道维度拼接输入一个全卷积网络输出一个两通道光流图。这种端到端网络的好处是实现简单前向推理快但精度受限于单次前向的“一次性估计”。问题在于“一次性估计”对大位移极其吃力。当物体运动超过几十个像素时需要匹配的信息跨越了很远的空间距离卷积核感受野覆盖不到网络只能靠猜测。而且光流任务天然是多模态的——同一个小区域可能有多个相似纹理会造成歧义匹配单次前向的输出本质上是一个“平均结果”会把可能的多个运动模式糊在一起。4.2 多尺度与金字塔结构先粗后细针对大位移问题业界很早就引入了“金字塔”思路把输入图像下采样成多个尺度先在最粗糙的尺度上估计一个大致位移然后逐层上采样细化每一层都在上一层基础上做残差修正。这种做法本质上模仿了传统方法的由粗到细策略只不过每层的细化过程由网络自己学习。金字塔方案在实际部署中要注意一个点下采样倍率不能太大否则粗糙层已经丢失了小物体的信息后面无论怎么细化都找不回来。我测试过4层金字塔、每层下采样2倍对中小位移效果很好但如果是画面里有一个快速移动的小球粗尺度图像上小球几乎消失光流预测就直接失败。后来我换成“同时输入原始尺寸多个下采样尺寸”并联的分支结构效果比串联金字塔好不少虽然计算量上去了但对小目标的召回率明显改善。4.3 迭代式优化架构RAFT这类思路为什么强当前主流的光流网络架构放弃了“直接回归光流”的思路改为“迭代优化”。核心思想是网络维护一个初始光流场通过反复迭代逐步修正这个光流场每次迭代都让结果更精细一些。这有点像优化算法里的梯度下降——只不过下降方向不是人为设计的而是网络自己学出来的。我特别看好这种架构的原因是它把“特征匹配”和“运动平滑”解耦了。网络内部有一个部分专门负责构建两帧之间的相关性——把第一帧特征和第二帧特征做点积匹配生成一个关于“候选位移”的匹配代价另一个部分负责根据这个代价和历史光流状态决定每一步迭代光流应该往哪个方向更新多少。分解之后特征匹配部分可以做得非常深迭代更新部分则像一个门控循环单元可以积累历史信息让更新过程更稳定。这种架构的训练方式和推理方式很不一样。推理时固定迭代次数通常8到12次训练时要随机采样迭代次数避免网络只会在固定次数下工作迫使网络每一步都尽可能多地学习有意义的信息。我一开始没有做随机迭代次数采样训练完推理到15步时发现继续迭代反而变差后来加上随机采样模型才真正学会“多迭代多收益”。4.4 轻量化改造在速度与精度之间找平衡如果你要把光流模型部署到嵌入式设备或实时视频流处理管线里直接上大规模迭代模型会非常吃力。我的做法是先保证主干网络的精度再做三个方向的轻量化改造一是降低输入分辨率。这是最直接有效的办法。输入分辨率降到一半计算量理论上是四分之一精度下降大约百分之十到十五。很多应用场景其实不算特别需要高精度比如视频插帧、运动分割降分辨率带来的误差完全可接受。二是用轻量级卷积模块替换标准卷积。具体来说把标准的3x3卷积替换成深度可分离卷积参数量和计算量都能降一个量级。代价是精度会有一点损失但如果配合蒸馏训练——用大模型输出的光流作为小模型的监督目标——精度损失可以大幅回收。三是对迭代模块做剪枝。迭代式架构在后期迭代中更新的幅度本来就很小存在大量冗余。常规做法是前几步用高分辨率特征后面逐渐缩减特征通道数或者直接让网络预测每步的“置信度”置信度低的步骤提前结束迭代。我在实际测试中把迭代次数从10次减到5次用置信度控制提前终止速度提升接近一倍精度只掉了百分之三左右非常划算。5. 训练实操从零跑通一个光流模型5.1 骨架搭建与输入输出约定我以自监督方案为例说一下完整的训练流程因为这种方案最贴近实际项目需求不需要真值。首先约定输入格式。训练数据是一个batch每个样本包含四部分当前帧图像x1、下一帧图像x2、网络预测的光流F、计算重建误差用的遮挡掩码M。图像统一归一化到0到1之间光流场F在进入损失函数前需要先乘以一个缩放因子把运动尺度映射到合适的数值范围。我常用缩放因子20也就是光流值在正负20像素范围内这样初始梯度幅度比较稳定。网络结构直接采用迭代式架构即可特征提取部分可以复用标准的残差结构迭代更新部分用门控循环单元实现。我给出的代码约定里网络输出的是32倍下采样分辨率的光流场最后通过凸上采样恢复全分辨率。凸上采样是当前光流网络常用的技巧它不是一个固定的双线性插值而是学习一个可分卷积核根据内容动态决定如何从粗糙光流插值到精细光流。这个模块对边缘保持效果明显比双线性插值能保留更多运动细节。5.2 损失函数实现细节与参考代码自监督光流的核心损失是重建损失加平滑损失。重建损失的输入是“预测光流F反扭曲下一帧得到重建帧”我这里给一个示意性的实现代码方便理解整体逻辑import torch import torch.nn.functional as F def compute_photometric_loss(flow, img1, img2, mask): # 根据光流场生成采样网格 B, _, H, W flow.shape yy, xx torch.meshgrid( torch.linspace(-1, 1, H, deviceflow.device), torch.linspace(-1, 1, W, deviceflow.device), indexingij ) # 注意光流需要归一化到采样网格范围 grid torch.stack((xx flow[:, 0] * 2 / W), dim-1) grid_y yy flow[:, 1] * 2 / H grid torch.stack((grid[..., 0], grid_y), dim-1) # x, y # 反扭曲第二帧得到重建帧 warped_img2 F.grid_sample(img2, grid, modebilinear, padding_modeborder) # 计算加权Charbonnier损失 diff img1 - warped_img2 robust_loss torch.sqrt(diff ** 2 1e-6) masked_loss (robust_loss * mask).sum() / (mask.sum() 1e-8) return masked_loss def compute_smoothness_loss(flow, img1): # 计算光流场的空间梯度 flow_u flow[:, 0] flow_v flow[:, 1] u_dx flow_u[:, :, 1:] - flow_u[:, :, :-1] u_dy flow_u[:, 1:, :] - flow_u[:, :-1, :] v_dx flow_v[:, :, 1:] - flow_v[:, :, :-1] v_dy flow_v[:, 1:, :] - flow_v[:, :-1, :] # 基于图像边缘的权重 img_gray img1.mean(dim1, keepdimTrue) edge_w_x torch.exp(-5.0 * (img_gray[:, :, 1:] - img_gray[:, :, :-1]).abs()) edge_w_y torch.exp(-5.0 * (img_gray[:, 1:, :] - img_gray[:, :-1, :]).abs()) smooth (edge_w_x * u_dx.abs()).mean() (edge_w_x * v_dx.abs()).mean() smooth (edge_w_y * u_dy.abs()).mean() (edge_w_y * v_dy.abs()).mean() return smooth这里有两个细节值得多说一句。第一warp操作里光流值必须经过归一化变换因为grid_sample的取值范围是-1到1需要先把像素位移换算到这个坐标系。第二遮挡掩码M在自监督模式下通常由网络自己预测也可以用一个“前向-反向一致性检查”粗略估计——把前向光流和反向光流叠加如果两条光流路径不能回到原点就认为该像素被遮挡。5.3 评价指标与可视化检查法训练过程中不能只盯着loss曲线看。Loss下降不代表光流正确这是光流任务最迷惑人的地方。我建议每训练一个阶段就保存一组验证图像上的可视化结果用颜色编码的方式查看光流场。颜色编码的方案也很常规光流方向对应色相光流大小对应亮度这样一张彩色图就能直观显示运动方向和幅度。最常用的量化指标是“终点误差”EPE它计算每个像素预测光流与真值光流的欧氏距离的平均值。如果没有真值可以用“前向-反向一致性误差”或者“重建误差”作为替代指标但这两个指标都有各自的盲区。我遇到的最典型情况是损失在降可视化结果也很平滑但光流场里小人物的腿被糊成一片完全看不出独立运动。这种问题只看量化指标是发现不了的必须单独看运动边界区域。所以我的建议是除了全图可视化还要做“边缘裁剪放大”——把图像边缘响应较高的区域切出来单独看光流颜色变化。颜色如果平滑过渡说明边界保持得不好需要加强边界感知的损失设计。6. 常见问题与排查技巧实录6.1 光流场看起来像“毛玻璃”边缘全糊了问题表现可视化图里物体内部颜色变化平缓但物体边缘和背景之间的颜色过渡也很平缓没有锐利的分界线。排查思路这通常是平滑损失权重过大或者网络表达能力不足导致网络“不敢”预测剧烈的边界跳变。常规做法是把平滑损失的边缘权重调大也就是让模型在检测到图像边缘时大幅降低平滑惩罚。如果你用的是固定边缘权重可以先可视化一下边缘权重图看它是不是真正覆盖了运动物体的轮廓。很多时候我们以为的边缘权重其实只识别了纹理边界对物体轮廓不敏感需要换更强的边缘检测特征。6.2 训练Loss震荡不下降甚至一直停在某个值附近问题表现Loss曲线从头到尾都在一个区间内剧烈波动没有明显的下降趋势。排查思路先看是不是损失权重失衡。把平滑损失和重建损失分别打出来看看哪一项在主导。如果平滑损失已经趋于零但重建损失还很大说明模型正在牺牲重建精度来换取平滑度。正常情况应该是重建损失占主导平滑损失起辅助作用。再看是不是数据增强过强特别是随机遮挡如果范围太大会导致大量像素被置为无效监督信号密度急剧下降网络学不到东西。第三步检查学习率调度是否过于激进——如果初始学习率太大梯度会在loss landscape里反复横跳需要捏小一个量级再试。6.3 光流场出现“条纹状伪影”一片区域全是并行短线问题表现可视化图上某一片区域出现规律性的短线状色块像刷子刷过一样光流方向几乎一致但不连续。排查思路这种现象多半出现在低纹理或者重复纹理区域比如墙面、铁轨、百叶窗。本质原因是网络在这种区域找不到可靠的匹配线索只能靠平滑约束猜测而在重复纹理区域多个位移都能产生相似匹配代价网络输出了一个“多峰平均”的糊弄结果。我的处理办法有三个方向一是增加全局上下文信息让网络根据物体语义来判断该区域内部的运动模式二是把特征匹配范围扩大减少重复纹理的歧义三是后处理加一个中值滤波把条纹伪影压掉。第三种方法虽然治标不治本但视觉效果改善很快适合快速上线。6.4 显存爆炸训练根本跑不起来问题表现训练刚开始GPU显存就满了或者前向传播可以但反向传播时报OOM。排查思路光流网络通常要保存计算图中的中间特征迭代式架构如果每一步都要保存梯度显存消耗会非常夸张。有一个被广泛采用的工程手段是“检查点”梯度策略——只在部分迭代步骤中保存中间激活值其他步骤丢弃反向传播时重新计算。这会让训练时间略增但显存占用可以下降接近一半。另一个常用手段是把光流迭代步数在训练时随机减半比如只用4到6步训练推理时再恢复到10步以上。这样做不仅省显存还能提升模型的泛化能力我实测下来精度没有明显下降算是我强烈推荐的一个配置。6.5 模型的“终点误差”很低但重建误差很高问题表现如果手头有真值数据量化指标EPE表现不错但自监督重建误差一直偏高。排查思路这说明网络在“数值上”预测得很准但生成中间特征的质量一般。常见原因是特征提取网络的学习率设置不当——光流主网络和特征提取网络需要不同的学习率特征提取部分往往需要更小的学习率才能保证特征的稳定性。另外一个原因是两帧图像之间的亮度差异很大导致重建误差天然就高这种情况可以先把图像做一次全局直方图匹配消除整体亮度偏置再进入网络。我个人在实际操作中最深的体会是光流训练的问题绝大多数出在数据端而非网络端。网络结构只要不是特别离谱靠增大数据多样性、增强常见的遮挡和光照变化效果都能往上提。我甚至试过用同一套网络结构只改数据增强策略在一个小规模测试集上把EPE指标提升了将近15%这个幅度比换任何网络结构都明显。最后再分享一个小技巧训练完成后强烈建议把模型在完全没见过的视频数据上做一次“零样本测试”——视频随便用手机拍一段就行不需要任何标注。重点观察人走动、车辆穿行、镜头快速转向这些场景下的光流可视化效果。这一步能快速暴露出合成数据训练留下的各种问题比看测试集指标靠谱得多。如果发现镜头快速移动时大区域光流发散不用急着改网络先检查一下数据里是否缺少“背景整体大位移”的样本这个问题通常加几段快速平移和旋转的合成数据就能解决。
阅读完成 · 觉得有帮助?