1. 从零开始:使用Flownet2生成optical flow
光流(optical flow)这个词,第一次听到的人多半会觉得抽象。说白了,它描述的就是视频里相邻两帧之间,每个像素从上一帧“跑”到了下一帧的哪个位置。你可以把它想象成给画面里每一个点都画一支小箭头,箭头方向代表运动方向,箭头长度代表运动快慢。整张箭头图就是光流场。这东西在视频插帧、动作识别、自动驾驶的障碍物运动估计、视频压缩、甚至电影后期里都有大量应用。
而Flownet2,是这个领域里一个绕不开的经典模型。它由多个子网络堆叠而成,专门用来从一对图像里回归出稠密光流。相比传统方法(比如Farneback、DIS),它在精度上有明显优势,尤其是大位移和复杂纹理场景。但它的部署和推理流程,对新手来说并不算友好——依赖多、权重文件大、输入格式讲究、显存吃紧。这篇内容就是把我自己从零搭起Flownet2、跑通光流生成、再到调优踩坑的完整过程整理出来。适合刚接触光流、想快速跑通一个baseline的开发者,也适合已经用过传统方法、想升级到深度学习方案的朋友。下面从整体思路开始拆。
2. 整体设计与方案选型思路
2.1 为什么选Flownet2而不是其他方案
光流估计的方案大致分三代。第一代是传统变分法,比如Horn-Schunck、Lucas-Kanade,优点是轻量、无需训练,缺点是对大位移和纹理缺失区域基本无能为力。第二代是判别式深度模型,Flownet系列就是代表,直接输入两张图,输出光流。第三代是近几年基于Transformer或迭代优化的方案,比如RAFT系列,精度更高但推理更重。
那为什么还选Flownet2?我的考量有三点。第一,它是经典基线,论文和开源实现成熟,社区里遇到问题容易搜到答案。第二,它的结构清晰,由FlowNetC、FlowNetS以及多个融合子网络组成,适合用来理解“从粗到精”的光流回归思想。第三,它的权重文件公开可得,不需要自己从头训练,对个人开发者和小团队友好。RAFT虽然强,但迭代推理的显存占用和延迟在普通显卡上不太友好,而Flownet2在合理分辨率下还能跑得动。
提示:如果你的场景对精度要求极高、且显卡充裕,可以后续再迁移到RAFT;但作为入门和baseline,Flownet2的性价比很高。
2.2 整体流程拆解
整个任务可以拆成四个阶段。第一阶段是环境准备,包括Python、PyTorch、CUDA以及Flownet2的依赖库。第二阶段是权重获取与模型加载,这里最容易卡住,因为官方权重是分模块的,需要正确拼接。第三阶段是数据预处理与推理,重点是图像尺寸对齐、归一化和通道顺序。第四阶段是光流可视化与结果校验,把浮点光流场转成能看的彩色图。
我建议按这个顺序推进,不要跳步。很多人一上来就想直接跑推理,结果卡在权重加载或者尺寸不匹配上,反而更费时间。先把环境跑通,再逐步验证每个环节的输出,出问题容易定位。
2.3 硬件与软件的基本预期
Flownet2对显存的要求取决于输入分辨率。以常见的512x384为例,推理时显存占用大约在2到3GB;如果上到1024x768,可能要到6GB以上。所以一张6GB显存的显卡是比较舒服的起点。CPU推理也能跑,但速度会慢一个数量级,不适合批量处理。
软件层面,PyTorch版本要和CUDA匹配,这是老生常谈但每次都会踩的坑。Flownet2的原始实现基于较老的PyTorch,直接在新版本上跑可能会遇到API变动。我的做法是锁定一个经过验证的版本组合,避免边调模型边调环境。
3. 环境搭建与依赖处理
3.1 Python与PyTorch版本选择
版本选择上,我实测下来比较稳的组合是Python 3.8搭配PyTorch 1.10左右的版本,CUDA用11.3。这个组合的好处是既有较新的算子支持,又不会因为太新而导致老代码报错。如果你用的是更新的PyTorch 2.x,部分旧写法(比如某些张量操作)可能需要微调,但整体逻辑不变。
安装PyTorch时,建议直接去官方渠道按CUDA版本选择对应命令,不要用pip默认装CPU版。很多人跑起来发现速度奇慢,最后查出来是装成了CPU版本。验证方法很简单,进入Python后执行一行检查CUDA是否可用的代码,返回True才算成功。
import torch print(torch.cuda.is_available()) print(torch.version.cuda)如果第一行输出False,说明CUDA没配好,先别急着往下走,把驱动和CUDA版本对齐再说。
3.2 Flownet2依赖库的安装
Flownet2的原始实现依赖几个额外的库,其中最关键的是用于相关层(correlation layer)的自定义CUDA扩展。这个扩展需要编译,而编译又依赖正确版本的CUDA工具链和C++编译器。这是整个搭建过程中最容易出问题的一步。
我的经验是,先确认系统里有nvcc,并且版本和PyTorch用的CUDA一致。然后进入Flownet2的networks目录,执行编译脚本。如果编译报错,八成是CUDA版本不匹配或者gcc版本太新。可以尝试降级gcc,或者用conda环境隔离出一套干净的编译工具链。
注意:编译自定义扩展时,报错信息往往很长,但真正有用的通常是最上面几行。先看第一个error,不要被后面的连锁报错带偏。
3.3 权重文件的获取与放置
Flownet2的权重是分模块的,包括FlowNet2、FlowNet2C、FlowNet2S等多个子模型。官方提供的是一个打包文件,解压后是一堆.pth文件。你需要把它们放到代码约定的目录下,通常是checkpoints文件夹。
这里有个细节:不同版本的代码对权重文件命名和路径的约定可能不同。如果加载时报“missing key”或者“unexpected key”,先别怀疑权重坏了,多半是路径或者命名对不上。打开加载权重的代码,看清楚它期望的文件名,然后对照实际文件重命名即可。
4. 核心细节解析与实操要点
4.1 输入图像的预处理规范
Flownet2对输入有明确要求。两张图像必须是同一尺寸,且尺寸最好是64的倍数,因为网络内部有多次下采样。如果尺寸不是64的倍数,可能会在拼接或上采样时出现维度不匹配。我的做法是先把图像缩放到最近的64倍数,推理完再把光流缩放回原尺寸。
通道顺序也要注意。OpenCV读进来默认是BGR,而PyTorch模型通常期望RGB。这个顺序搞反了,光流结果会明显异常,但又不一定报错,属于隐蔽的坑。归一化方面,Flownet2期望输入在0到255范围,而不是0到1,这点和很多其他视觉模型不同,务必看清代码里的预处理逻辑。
4.2 光流的输出格式与含义
模型输出的光流是一个两通道的浮点张量,第一个通道是水平方向的位移(u),第二个通道是垂直方向的位移(v)。单位是像素。正值表示向右或向下移动,负值表示向左或向上。这个张量的尺寸和输入图像一致,每个位置对应一个位移向量。
理解这一点很重要,因为后续无论是做可视化还是做warp,都依赖这个格式。比如你想把第一帧按光流warp到第二帧,就是用这个位移场去采样。如果通道顺序理解错了,warp出来的结果会完全错位。
4.3 光流可视化的标准做法
原始光流是浮点数,直接看是一团乱。标准做法是用颜色编码:色相(Hue)表示方向,饱和度或明度表示大小。业界常用的可视化方案是把光流向量转成角度和模长,角度映射到色相环,模长映射到亮度。
实现上,可以用numpy手动算,也可以借助现成的可视化函数。手动算的好处是可控,比如你可以对模长做归一化,避免个别超大位移把整体亮度压暗。我一般会把模长按95百分位做截断,这样可视化更均衡。
import numpy as np import cv2 def flow_to_color(flow): h, w = flow.shape[:2] hsv = np.zeros((h, w, 3), dtype=np.uint8) mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1]) hsv[..., 0] = ang * 180 / np.pi / 2 hsv[..., 1] = 255 hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX) return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)这段代码是很多项目的通用写法,实测稳定。注意cartToPolar的输入顺序是x在前、y在后,和光流通道顺序一致。
4.4 显存与分辨率的权衡
分辨率越高,光流越精细,但显存和耗时也线性上升。我的建议是先用小分辨率(比如320x240)跑通全流程,确认结果合理后再逐步提高。如果显存不够,可以分块推理,但分块会在边界产生不连续,需要额外做融合,比较麻烦。
另一个技巧是推理时用torch.no_grad()包起来,避免保存中间梯度,能省下不少显存。这个细节很多人会忘,尤其是从训练代码改过来的时候。
5. 完整实操流程与关键环节
5.1 从两张图到光流场的完整代码
下面是我整理的一套可复现流程。假设你已经装好环境、放好权重,并且有两张同尺寸的图像。
import torch import cv2 import numpy as np from networks import FlowNet2 def load_model(weight_path, device): model = FlowNet2() state = torch.load(weight_path, map_location=device) model.load_state_dict(state['state_dict']) model.to(device) model.eval() return model def preprocess(img1_path, img2_path, target_size=(512, 384)): img1 = cv2.imread(img1_path) img2 = cv2.imread(img2_path) img1 = cv2.resize(img1, target_size) img2 = cv2.resize(img2, target_size) img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB) img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB) img1 = torch.from_numpy(img1).permute(2, 0, 1).float() img2 = torch.from_numpy(img2).permute(2, 0, 1).float() return img1.unsqueeze(0), img2.unsqueeze(0) def infer(model, img1, img2, device): img1 = img1.to(device) img2 = img2.to(device) with torch.no_grad(): flow = model(torch.cat([img1, img2], dim=1)) return flow.squeeze(0).permute(1, 2, 0).cpu().numpy()这段代码的关键点有三个。第一,权重加载时用的是state_dict,不同版本的权重结构可能略有差异,如果报错就检查key的前缀。第二,预处理里把BGR转成了RGB,这一步不能省。第三,推理时把两张图在通道维度拼接,这是Flownet2的标准输入方式。
5.2 参数选择与尺寸计算过程
假设原图是1920x1080,直接送进网络显存肯定爆。我的缩放策略是保持长宽比,把长边缩到512,短边按比例算。1080除以1920约等于0.5625,512乘以0.5625等于288。288不是64的倍数,向上取到320。所以最终尺寸是512x320。
推理完得到的光流是512x320的,要还原回1920x1080。这里不能简单resize,因为光流的数值是像素位移,缩放图像尺寸时位移也要按比例缩放。水平方向缩放倍率是1920/512=3.75,垂直方向是1080/320=3.375。所以光流的u通道要乘以3.75,v通道乘以3.375。这一步如果漏了,warp结果会严重错位。
5.3 结果校验的实用方法
跑出光流后,怎么判断它对不对?最直接的方法是做warp校验:用第一帧和光流,把第一帧warp到第二帧的位置,然后和真实的第二帧对比。如果光流准确,warp结果应该和第二帧高度相似。差异大的区域,往往就是光流估计失败的区域,比如遮挡、无纹理、运动模糊。
我一般会同时输出三张图:第一帧、第二帧、warp结果。肉眼一看就能判断光流质量。如果warp结果整体偏移,多半是通道顺序或者缩放倍率搞错了。如果局部糊成一团,那是模型本身的局限,属于正常现象。
5.4 批量处理的工程化建议
单张推理跑通后,如果要处理视频,就需要批量。我的做法是先把视频拆帧,然后逐对相邻帧推理,最后把光流存成npy或者可视化后的视频。批量处理时要注意显存释放,每处理完一批就手动清一下缓存,避免长时间运行后显存碎片化导致OOM。
另外,视频帧之间的光流有连续性,如果对时序一致性要求高,可以考虑用前后向一致性校验,把不可靠的光流区域标记出来。这个属于进阶优化,入门阶段可以先不做。
6. 常见问题与排查技巧实录
6.1 权重加载失败的几种典型情况
权重加载报错是最常见的问题,我整理了一个速查表。
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
| Missing key(s) | 权重文件不完整或版本不匹配 | 确认权重是完整打包版,检查key前缀 |
| Unexpected key(s) | 代码结构和权重结构不一致 | 去掉多余前缀,或改用匹配的代码版本 |
| CUDA out of memory | 分辨率过高或未清缓存 | 降低分辨率,加no_grad,手动清缓存 |
| Size mismatch | 输入尺寸不是64倍数 | 缩放到64的倍数再推理 |
这张表是我踩坑后总结的,基本覆盖了八成以上的加载问题。遇到报错先对号入座,比盲目搜索效率高。
6.2 光流结果异常的排查思路
如果光流跑出来了但结果明显不对,按这个顺序排查。第一,检查输入图像是否同尺寸、同通道顺序。第二,检查归一化范围是不是0到255。第三,检查光流缩放倍率是否和图像缩放一致。第四,检查可视化时的通道顺序。这四步走完,大部分异常都能定位。
我遇到过一次光流整体偏绿的情况,查了半天发现是可视化时把u和v通道搞反了。这种问题不会报错,只能靠肉眼和逻辑判断。
6.3 性能优化的几个实用技巧
推理速度上,有几个立竿见影的优化。第一,用半精度(fp16)推理,速度能提升接近一倍,精度损失在光流任务上通常可以接受。第二,把模型设为eval模式,关闭dropout和batchnorm的训练行为。第三,如果只是做可视化,可以把输出光流降采样后再可视化,减少后处理耗时。
显存优化上,除了no_grad,还可以用torch.cuda.empty_cache()定期清理。如果还是不够,就老老实实降分辨率,这是最有效的办法。
提示:半精度推理在部分老显卡上可能不支持,先用一小段代码测试,确认没问题再全量跑。
6.4 关于自定义CUDA扩展编译失败
这个问题的根源通常是编译环境不干净。我的建议是用conda建一个独立环境,只装必要的编译工具,避免和系统里的其他版本冲突。如果实在编译不过,可以找找有没有预编译好的轮子,或者用纯Python实现的相关层替代(速度会慢,但能跑通)。
编译失败时,重点看第一个error。常见的是gcc版本太新导致语法不兼容,降级到gcc 7或8通常能解决。另一个常见原因是CUDA路径没配好,检查CUDA_HOME环境变量是否指向正确的安装目录。
7. 我个人在实际操作中的几点体会
Flownet2这套东西,第一次搭确实会花不少时间,但一旦跑通,后面就是重复劳动。我的建议是先把整个流程用最小的分辨率跑一遍,确认每个环节的输出都符合预期,再逐步放大。很多人一上来就追求高分辨率高精度,结果卡在环境问题上耗尽了耐心。
另外,光流这个领域更新很快,Flownet2是很好的起点,但不要把它当成终点。跑通之后,可以拿它和传统方法做个对比,看看深度模型到底在哪些场景下更有优势。这种对比做多了,你对光流的理解会从“会跑代码”上升到“知道什么时候该用什么方法”。
最后分享一个小技巧:如果你手头的视频帧率很高,相邻帧位移很小,其实可以隔帧取图来增大位移,这样光流估计反而更稳。这个技巧在慢动作视频处理里特别有用,实测下来效果不错。