1. 光流为什么需要深度学习:从像素匹配到语义先验
1.1 问题定义:给每个像素发一张“运动工牌”
先花三十秒把光流这事讲清楚。给定视频里相邻两帧图,第一帧的某个像素在第二帧里跑到哪个位置了?把这种移动量按像素位置全部算出来,输出一张和原图大小一致的二维矢量图,每个像素点带一个(dx, dy),这个结果就是光流场。深度学习估计光流,就是训练一个神经网络,输入两帧图,直接回归出这张稠密运动场。
光流和稀疏特征点跟踪是两码事。特征点跟踪只关心角点、纹理丰富的少数位置,光流要求画面里每一个像素都有运动量。你可以把它理解成给每个像素发一张“运动工牌”,上面写着往左还是往右、移了几个像素。这个输出形态决定了它天然是重活,也是很多上层任务的命根子:视频插帧要按运动矢量来补中间帧,运动目标分割要用运动边界来切对象,自动驾驶要算障碍物的相对速度,SLAM要借助光流做帧间约束。
难点在哪?很多像素其实根本没有独有特征。一面白墙的中间区域,镜头平移一下,它看起来和原来一模一样,网络没有任何局部线索判断它到底往哪走了。这个病态问题在视觉领域有正式名字,叫孔径问题,可以类比成:你透过一个圆筒去看一根竖条纹的杆子,杆子上下移动和左右移动在筒眼里长得一样,你根本分不清。传统方法在这个位置只能靠周围像素来“传染”运动量,一旦周围也没有纹理,整片区域就只能靠猜。
1.2 传统方法的天花板
在深度学习火起来之前,光流估计基本被两类方法统治。一类是局部方法,代表是Lucas-Kanade,它假设一个小窗口内像素运动一致,用最小二乘去解稠密运动。这类方法算得快但在运动不连续的边界处一团糟,窗口开大了糊边,开小了纹理不够解不出结果。另一类是全局方法,代表是Horn-Schunck,它把光流建模成能量最小化问题,数据项保证像素匹配,平滑项保证邻居运动一致,然后迭代求解。
这两条路线有个共同短板:它们对世界的假设太简陋。亮度恒定假设在光照突变、阴影边缘、曝光自动调节的场景里直接失效;平滑假设在物体边缘、遮挡边界、快速旋转处天然违背。更麻烦的是,遮挡区域的像素在下一帧里根本没有对应点,传统优化框架在这个位置无论如何也算不出可靠值,只能靠平滑项硬糊。你可以把传统光流想象成一个近视眼选手,看不清远距离细节,只好靠“周围差不多”这一点来蒙答案,碰上周围也模糊,就彻底摆烂了。
深度学习的切入方式完全不同。它不再手写物理假设,而是用大量带真值的光流数据去训练一个端到端网络,让模型自己从数据里总结出什么样的外观变化对应什么样的运动。更关键的是,卷积网络有非常大的感受野,它看一个像素时,视野里装下了周围几十上百个像素的上下文,甚至能跨帧看到远处结构——这就让它有机会在无纹理区域利用语义信息做推断。比如看到一个按钮在墙上,墙虽然白花花一片,但网络可以根据按钮位置的移动推算出整面墙大概是怎么运动的。这件事在传统方法里几乎不可能建模,在深度学习里却顺理成章。
2. 十年演进线:FlowNet、PWC-Net到RAFT各自解了什么题
2.1 FlowNet的暴力美学
深度学习做光流的开山之作思路异常直白:把两帧图像沿通道方向拼起来,喂进一个U型卷积网络,输出端直接回归两通道的光流图,中间没有任何手工设计的匹配模块。用当时的话说就是暴力美学——收集大量合成数据,让网络自己从输入到输出端到端硬学。
FlowNet试了两种结构。FlowNetS最简单,直接concat两帧,好处是实现快、结构干净;FlowNetC稍微讲究一点,先分别提特征再计算两块特征的相似度,相当于在网络里嵌入了一个显式的匹配层。当时的实验结果放在今天看不算惊艳,但价值在于证明了端到端学习光流这件事完全可行,而且速度比传统变分方法快两三个数量级。从训练技巧角度,我印象最深的是它们引入了多尺度损失,对不同分辨率的光流预测都算损失并反传,这个思路后来被几乎所有光流乃至立体匹配网络继承。
FlowNet真正暴露的问题是细节精度不够。大位移目标能估个大概方向,但轮廓边缘过度平滑,小物体直接被吞掉。原因也清楚:纯卷积堆叠时,高层特征已经很抽象,空间位置被严重压缩,回归出来的光流缺乏亚像素级别的精修能力。这个阶段更像是给大家趟了条路:路能走,但坑坑洼洼,需要后面的工作补细节。
2.2 PWC-Net:把“搜索对应点”这件事讲明白
PWC-Net在结构上比FlowNet收敛了一个关键思想:与其让网络黑盒式地直接回归运动,不如把传统方法里的“特征金字塔+对应关系搜索+基于搜索结果的运动估计”显式拆出来。PWC三个字母分别代表金字塔(Pyramid)、扭曲(Warping)、代价体(Cost Volume)。
它的做法是:先构造图像金字塔,粗尺度上算大位移,细尺度上补小位移;每一层根据上一层传下来的粗糙光流把第二帧特征扭曲回第一帧视角,相当于先做一个初始对齐;然后在对齐后的特征上计算局部相关,得到一个代价值。网络拿到代价值后再回归光流残差,用来修正上一层的光流。这个“先粗对齐再搜残差”的思路,本质上就是迭代逼近,可以说为后面RAFT的迭代细化埋下了伏笔。
PWC-Net的好处是参数量很小、计算快、每一层的设计都能对应到物理含义,调试时容易定位到底是哪一层的运动估计出了问题。它和FlowNet相比最大的进步在于:不再让网络自己发明匹配方法,而是用一个高度可解释的模块代替。代价体这个东西,其实就是在回答“特征点移动后和周围哪些位置长得更像”,是一种显式的相似度查询。后续几乎所有高精度模型都沿用了这个模块,只是查询和使用的复杂度不同。
2.3 RAFT:迭代细化和相关体为何能拉开差距
RAFT的出现在我当时看来是一个分水岭。它的核心是构建了一个4D相关体(2D特征图对应2D特征图,构成四维张量),里面保存了第一帧特征图和第二帧特征图所有位置两两之间的相似度。这等于把“两帧之间所有可能的匹配分数”一次性全部算出来,网络后面想查哪个位置,随时都能查到。
真正拉开差距的是迭代更新策略。RAFT不是一次性输出光流,而是维护一个光流估计的中间状态,用一个类似GRU的循环模块反复读取当前光流附近的相关值,不断更新光流场的预测。你可以把这个过程理解成写文章:FlowNet是一口气写完初稿直接交卷,RAFT是写完初稿反复阅读、每读一次发现哪里不顺畅就改哪里,迭代次数越多,文章越精致。推理时可以控制迭代次数,精度和速度之间自由滑动,这个特性在工程上非常实用。
从训练角度看,RAFT在每个迭代步上都会计算损失,而且使用指数递减的权重,让早期迭代粗修、后期迭代精修。这会逼着网络学会一条优化的路径,而不是只学一个终点。相关体做得这么重,内存开销自然大,但换来的是遮挡和微小结构位置上的明显精度提升。后续的GMA在这个框架上进一步加了全局运动注意力,专门处理被遮挡区域的“运动淹没”问题。可以看到,光流深度学习方法花了五年时间,终于从“直接回归”走到了“显式匹配+循环精修”,这也是今天主流做法的标准范式。
3. 训练一套光流模型:数据、指标和最小配置
3.1 没有真值怎么做数据:合成数据的课程学习
光流训练面临一个残酷现实:真实拍摄的视频几乎没有逐像素运动真值。给视频标光流,需要标注人员逐帧给每个像素指派位移,这在物理上就做不到。行业里的通行做法是使用合成数据——用计算机图形学渲染出带完整运动场的数据集。比如让一堆随机物体在随机背景上自由运动,渲染时引擎直接给出每个像素的真实位移,真值要多少有多少。
这类合成数据的代表性特点是:动画感强、纹理丰富、运动模式简单。直接拿它训练出的模型,迁移到真实视频上效果会明显打折,因为真实场景有运动模糊、传感器噪声、光照突变,还有复杂的遮挡嵌套。于是训练流程通常采用课程学习策略:先用简单合成数据训练到收敛,让模型学会基本的运动匹配逻辑;然后换用更复杂、更接近真实感的仿真数据继续训练,让模型适应遮挡和大幅运动;最后再用真实场景构建的训练数据微调,把分布拉回实际业务环境。一个跳过课程学习直接上个例场景微调的模型,往往会在陌生纹理上出现大面积的空洞区,这点我踩过。
合成数据还有个好处是可以做程序化数据增强。除了常规的翻转、旋转、裁剪、颜色扰动,还可以随机遮挡图像中的小块区域,模拟出前景被背景盖住的视觉状态,强迫网络学会在没有对应点时利用上下文推断。这类增强在光流训练里和网络结构同等重要,甚至更重要。
3.2 评估指标选哪个:EPE与F1-all的取舍
光流估计的评估指标里,最常用的是平均端点误差EPE,计算方法是:将预测光流和真值光流视为二维向量,逐像素求欧氏距离,再对整幅图取平均。EPE的优点是直观,一个像素如果预测偏移了1个像素,它就贡献1个像素的误差。缺点也很明显:一旦画面里存在少数极端错误的像素,它们会被平均值稀释掉。一个背景完全正确、前景主体全部预测错的模型,EPE可能看起来还过得去,但实际用途中前景恰恰是核心。
所以在真实场景基准上,大家更关注F1-all这种指标,它统计的是“误差超过3像素或误差超过该点真实位移5%”的像素占比。这个指标更贴近工程判断:光流是拿来用的,个别像素多点误差,只要能控制在阈值内就不算事。我的习惯是两类指标一起看,EPE反映整体水平,F1-all反映坏点比例。部署前后端任务时还要额外看运动边界位置的误差,因为很多下游算法对边缘处的光流不连续非常敏感。
3.3 一份可以上手的训练配置
以现在常见的RAFT式网络为例,我给一套可以直接复现的配置思路。输入方面,训练时候通常将图像随机裁剪到某个固定大小,比如横向约768像素、纵向约368像素的长方形patch,这样能塞进显存,也能覆盖多样化的局部运动。优化器一般选AdamW,初始学习率设在2e-4附近,配合周期性学习率调度,让学习率在训练过程中先升后降,方便在初期快速搜索、后期精细收敛。
损失函数通常对每次迭代的光流预测都计算L1距离,并按迭代次序给予递减权重,比如越靠后的迭代权重越高,这样网络会将重心放在最终精修结果上。训练流程上先在简单的合成数据上跑十万步左右,再切换到复杂合成数据继续训练十万步,最后用真实场景的小数据集微调几万步。推理时迭代次数默认设置在12到32之间,次数越多精度越高,速度按线性下降。如果没有特殊要吃满精度的场景,我通常设24次左右。这套配置我已经在多个内部项目里验证过,稳定性和精度都不错。
4. 从论文到生产:实测中最容易踩的六个坑
4.1 分辨率不匹配会让“训练失效”
训练时图像被随机裁剪成小patch,测试时往往直接喂整帧1080p甚至更高分辨率。这不是简单的显存问题,而是尺度语义发生了漂移。模型在训练中见过的物体尺度、纹理密度、运动尺度都是围绕patch尺寸来的,一旦整帧推入,网络感受野对应的实际物理范围发生了成倍变化,小物体和大位移的响应会变得异常。强行走一个没做任何分辨率适配的模型,轻则边缘模糊,重则整片产生错误运动矢量。
正确的姿态是推理时也要保持和训练一致的尺度:把输入图像先缩放或裁剪到模型熟悉的尺度范围,算完光流再把结果映射回原分辨率。更高分辨率的场景宁可切成带重叠的patch分块推理,用加权平均融合重叠区域,也不要无脑整帧直塞。这么做虽然麻烦,但能避免绝大部分的分辨率敏感问题。
4.2 遮挡区域:不是噪声,是系统误差
光流模型最惨烈的地方永远在遮挡边界。前景物体向右移动时,它左侧露出来一块原本被挡住的背景,这块区域在第一帧里根本不存在对应像素。网络没有任何线索能算出真实运动,只能基于上下文猜。这种情况下预测出的光流和真值差距巨大,而且这种误差在所有模型里都是系统性存在,不是靠调参能消除的。
工程上处理遮挡的标准动作是前向后向一致性检查:让模型从第一帧到第二帧算一次光流,再从第二帧到第一帧反向算一次,把两道光流场在同名点处做差值。如果两者方向相反且幅度相近,说明该点匹配是可靠的;如果出现明显矛盾,说明该点大概率处于遮挡或重复纹理区域。基于这个置信度图,下游任务可以选择忽略这些位置的光流,也可以结合边缘检测和分割结果对运动边界做重映射。我在多个视频插帧项目里都用过这个方案,它比直接盲目相信网络输出靠谱得多。
4.3 显存不够是常态,迭代次数是杠杆
光流网络吃显存不是一般地狠。两帧图像同时经过特征提取,还要构建高分辨率相关体,稍微大一点的输入立刻把显存占满。实际部署时,如果batch大小被压到1仍然放不下输入,优先调整的不是网络结构,而是迭代次数和输入分辨率。迭代次数从24降到8,精度损失在多数场景下小于3%,但推理速度能提升近一半。另外可以把相关体按块计算,需要哪块查哪块,避免一次性把完整相关体展开。
还有一个小技巧是混合精度推理。光流输出对数值精度没那么敏感,FP16推理在绝大多数应用里不会造成可见退化,但显存占用和计算时间都能明显下降。如果下游任务对光流的亚像素精度要求极高,再把输出层单独保留成FP32就行。
4.4 新场景微调数据从哪来:弱标签和自监督
真实业务里要换到一个全新领域,比如内窥镜画面或水下机器人视频,手头往往没有任何光流真值。直接硬跑通用模型,效果未必差,但肯定不是最优。没有真值时还有两条路。一条是弱标签路线:用传统稀疏特征点跟踪方法,比如LK跟踪加上RANSAC优化,在视频里挑出几百个稳定特征点,用跟踪结果作为稀疏监督,配合平滑约束来指导模型在新场景上微调。这个办法能纠正模型在特定纹理和光照下的系统性偏差,一次微调跑几万步就够。
另一条是自监督路线:利用两帧之间预测的光流把第一帧像素采样到第二帧位置,计算采样后图像和真实第二帧的像素差异,加上平滑项作为损失。这个办法胜在没有标注成本,问题是在无纹理区域容易解出全零或过平滑的解。实际项目里,两条路可以混合:先用自监督损失做初始化,再用稀疏弱标签做校正,稳定性和精度都好不少。
4.5 帧率变化让运动尺度规则失效
同一个深度学习光流模型,在30fps视频上效果不错,拿到120fps的慢动作视频上精度反而变差,这是很多人没预料到的。原因在于光流估计的本质是帧间位移,帧率翻倍意味着同样物理速度的目标每帧只移动原来一半像素,模型训练时见过的运动分布完全不匹配。反过来,无人机和高速工业相机的画面里每帧位移巨大,小感受野模型根本追不上。
解决方案通常有三种:一是按固定时间间隔跳帧输入,比如隔两帧或三帧计算一次运动,把运动尺度拉回模型熟悉的区间;二是对输入图像做下采样再放大光流,等效降低像素运动量;三是选用做了多尺度处理或迭代更新次数更多的模型。其中前两种方式成本最低,跳帧之后只要对光流做时间归一化处理即可,日常项目里够用。
4.6 输出后处理:别让一个离谱像素毁了下游
再好的光流网络,输出里也总会混入一些明显离谱的点。应用端如果直接拿原始光流场去算物体运动,一个飞点就可能把后续模块带偏。我建议输出后至少过三道轻量处理:首先用前向后向一致性检查生成置信度图;然后对低置信度区域做中值滤波,或参考周围高置信度像素做插值修复;最后在边缘区域做一次各向异性平滑,避免跨越物体边界的运动被抹平。
针对遮挡区域还有一种直观修法:先用深度信息或分割结果划出每个独立物体区域,然后在物体内部对光流做平面拟合或拉普拉斯平滑。这样就可以把那些孤立错乱的像素拉回合理的运动场中,也保留物体之间的不连续性。这类后处理在所有项目中都是普通但有效的一层保险,相比之下,只调网络参数来消除所有离群点往往事倍功半。
5. 选型对照:我把常用模型按场景分了类
| 模型路线 | 参数量量级 | 核心优势 | 典型适用场景 |
|---|---|---|---|
| 早期端到端卷积网络 | 约100M以上 | 结构简单,容易上手,适合快速原型验证 | 对精度不敏感的前期探索 |
| 金字塔+代价体网络 | 约10M以下 | 轻量高效,显存友好,可解释性强 | 移动端、实时性要求高的场景 |
| 迭代细化网络 | 约5M-10M | 精度天花板高,迭代次数可调 | 高精度离线处理、视频插帧、科研对比 |
| 带全局注意力模块的变体 | 基于迭代网络略增 | 遮挡区域鲁棒性更好 | 复杂遮挡场景,比如拥挤人群、密集车辆 |
这里要特别提醒一点:参数量小不代表精度一定差。把模型做小的同时如果保留迭代细化机制,效果往往好过一个大而粗糙的网络。选型时应该先看应用对精度的敏感度和允许的推理时间,再看显存和硬件约束。原型阶段建议直接上精度高的迭代细化模型,把效果验证清楚后再做蒸馏和剪枝,这个路径踩坑成本最低。反过来,如果需求本来就只需要做前景和背景的粗略区分,轻量模型就够了,堆高精度模型只会让部署成本失控。
我个人的实际感受是,深度学习光流发展到今天,最难的部分早就不是网络结构本身,而是数据适配和工程细节。一个在公开数据集上效果惊艳的模型,落到具体业务里,通常要先经历分辨率适配、遮挡处理、置信度过滤、微调数据整理这几关。尤其是微调数据的获取,多数团队在这里卡的时间最长。如果你也正要上光流方案,建议先花时间把你目标场景的视频收集起来,哪怕先用自监督方式做一轮粗对齐,也比拿通用模型硬顶要强得多。最后再分享一个技巧:当你发现新场景微调后效果忽好忽坏,第一件事检查图像归一化和输入尺度的设置,很多所谓的模型失效其实都源于这两个最不起眼的环节。