深度学习光流估计实战:Flownet2环境搭建、推理与可视化全流程
1. 光流估计到底在解决什么问题光流Optical Flow这个概念第一次接触的人很容易被名字唬住其实说白了就一句话算出视频里每个像素从上一帧到下一帧跑到了哪里。你拿手机拍一段路上行人走动的视频画面里每个人的位置在连续两帧之间都会发生位移光流要做的就是给画面里每一个像素点都分配一个二维向量 (dx, dy)告诉你在 x 方向和 y 方向上分别移动了多少。整张图所有像素的位移向量合在一起就形成了一张稠密的光流场图。这件事听起来简单但真正做起来难度不小。难点在于几个方面第一遮挡问题一个人走到另一个人后面被挡住了被遮挡区域的像素位移是无法直接观测的第二大位移问题相邻两帧之间物体移动了几十个像素传统基于局部窗口匹配的方法直接失效第三纹理缺失区域比如一面白墙或者纯色桌面局部区域没有任何可区分的特征算法根本不知道这块该往哪移。Flownet2 这个网络就是专门针对这几个痛点设计的。Flownet2 是 2017 年提出的一个基于深度学习的光流估计网络它的核心贡献在于用堆叠式网络结构stacked network来解决大位移问题。第一代 FlownetFlownet 1.0只有单一网络处理大位移时力不从心。Flownet2 把多个子网络串联起来第一个网络先出一个粗糙的光流估计第二个网络拿第一帧、第二帧以及第一个网络的输出作为输入专门去修正残差如此反复迭代。这个思路有点像数值计算里的迭代求精每一步都在前一步的基础上把误差缩小。我这次要复现的场景很典型手头有一段短视频素材需要提取稠密光流场用于后续的运动分析。选 Flownet2 而不是 OpenCV 自带的 Farneback 或者 DIS 光流原因很直接——深度学习方法的精度在复杂场景下明显更高尤其是存在大位移和非刚性形变的时候。Farneback 这类传统方法在纹理丰富、位移小的场景下够用但一旦画面里有快速运动或者遮挡结果就碎得没法看。Flownet2 虽然推理速度慢一些但精度上的优势是碾压级的。这篇文章适合谁看如果你正在做视频分析、运动估计、视频插帧、动作识别相关的工作或者单纯想跑通一个深度学习光流估计的完整流程那这篇内容可以直接抄作业。我会从环境搭建开始一步步走到最终生成可视化光流图中间踩过的坑和绕过的弯路都会写清楚。整个流程基于 2021 年初的环境虽然时间过去几年了但核心步骤和思路依然有效只是部分依赖库的版本可能需要微调。2. 环境搭建与依赖安装的完整思路2.1 为什么环境配置是最大的拦路虎Flownet2 这个项目比较特殊它原始代码是基于 Caffe 写的后来有人做了 TensorFlow 和 PyTorch 的复现版本。Caffe 版本在 2021 年已经很难编译了各种依赖冲突能把人逼疯。所以我当时的选择很明确用 PyTorch 复现版本。PyTorch 版本的好处是依赖清晰、社区活跃、遇到问题容易搜到解决方案。但即便是 PyTorch 版本环境配置依然有几个关键点需要注意。首先是CUDA 和 cuDNN 的版本匹配Flownet2 的 PyTorch 实现里用了自定义的 CUDA 扩展correlation layer 和 resample layer这些扩展对 CUDA 版本有要求。我当时用的是 CUDA 10.1 cuDNN 7.6.5PyTorch 1.4.0。这个组合在 2021 年初是比较稳的。如果你用更新的 CUDA 版本自定义扩展的编译可能会报错需要手动改一些源码里的宏定义。其次是Python 版本建议用 Python 3.6 或 3.7太新的版本3.9在一些老依赖上会有兼容性问题。我当时用的是 Python 3.7.9配合 Anaconda 管理环境这样隔离性好不会污染系统环境。2.2 一步步搭建可运行的环境先创建一个独立的 conda 环境conda create -n flownet2 python3.7.9 conda activate flownet2然后安装 PyTorch。注意这里要指定版本不要直接pip install torch装最新版pip install torch1.4.0 torchvision0.5.0接下来安装其他依赖pip install numpy1.19.5 pip install opencv-python4.5.1.48 pip install matplotlib3.3.4 pip install scipy1.5.4 pip install tensorboardX2.1这些版本号不是随便写的是我当时实测能跑通的组合。特别是 numpy 的版本太新的 numpy 和 PyTorch 1.4.0 会有 ABI 兼容性问题报numpy.ndarray size changed这种错误。2.3 编译自定义 CUDA 扩展的注意事项Flownet2 的 PyTorch 版本里有两个核心的自定义层correlation和resample。这两个层是用 CUDA 写的需要编译。编译命令通常在networks/目录下cd flownet2-pytorch/networks python setup.py install编译过程中最常见的报错是nvcc 找不到或者CUDA 架构不匹配。如果报 nvcc 找不到需要把 CUDA 的 bin 目录加到 PATH 里export PATH/usr/local/cuda-10.1/bin:$PATH export LD_LIBRARY_PATH/usr/local/cuda-10.1/lib64:$LD_LIBRARY_PATH如果报架构不匹配需要在setup.py里手动指定-archsm_XXXX 是你的 GPU 计算能力。比如 GTX 1080 Ti 是 sm_61RTX 2080 Ti 是 sm_75。这个参数不指定的话编译出来的扩展可能在你显卡上跑不了。提示编译自定义扩展时如果遇到error: identifier AT_CHECK is undefined这类报错说明 PyTorch 版本和源码不匹配。AT_CHECK 在 PyTorch 1.5 之后被移除了需要把源码里的 AT_CHECK 改成 TORCH_CHECK。这是 PyTorch 版本升级带来的常见坑。3. 模型权重下载与网络结构解析3.1 权重文件的获取与校验Flownet2 的预训练权重有好几个版本FlowNet2、FlowNet2-C、FlowNet2-S、FlowNet2-CSS、FlowNet2-CSS-ft-sd、FlowNet2-SD。这些后缀代表不同的网络配置简单说一下区别FlowNet2-C只用了 Correlation 层网络较浅速度快但精度一般FlowNet2-S只用了 Simple 层速度最快但精度最低FlowNet2-CSSCorrelation Simple Simple 的组合平衡了精度和速度FlowNet2-CSS-ft-sd在 CSS 基础上用 Small Displacement 数据集微调过对小位移更敏感FlowNet2-SD专门针对小位移场景FlowNet2完整的堆叠网络精度最高但速度最慢我这次用的是完整的FlowNet2权重因为我的场景里既有大位移也有小位移需要网络有全面的处理能力。权重文件大概 600MB 左右下载下来后需要校验一下 MD5确保文件没损坏。损坏的权重文件加载时会报unexpected EOF或者invalid load key这类错误。权重文件放到checkpoints/目录下然后在代码里指定路径加载。加载的时候有个细节PyTorch 的 state_dict 加载需要严格匹配键名如果网络定义和权重文件的键名对不上会报Missing key(s) in state_dict。Flownet2 的 PyTorch 实现里网络定义和官方权重是匹配的但如果你自己改了网络结构就需要手动处理键名映射。3.2 网络结构的核心设计逻辑Flownet2 的网络结构可以拆成几个关键组件来理解第一层是特征提取。输入的两帧图像先分别经过几个卷积层提取多尺度的特征。这里用的是类似 U-Net 的编码器结构逐步下采样扩大感受野。为什么要下采样因为光流估计需要大范围的空间上下文信息如果只在原分辨率上做卷积感受野太小网络看不到足够大的区域大位移就估计不出来。第二层是 Correlation 层。这是 Flownet 系列的核心创新点。传统的卷积网络做光流估计时是把两帧特征直接拼接concatenate然后卷积这种方式网络需要自己学会匹配两帧之间的对应关系学习难度大。Correlation 层直接计算两帧特征图之间的相关性相当于显式地告诉网络“第一帧的这个位置和第二帧的哪个位置最相似”。这个操作大大降低了学习难度也提高了精度。第三层是光流解码器。经过 Correlation 层之后网络得到了匹配代价体cost volume然后通过一系列上采样和卷积操作逐步恢复分辨率最终输出和原图同尺寸的光流场。这里用了refinement 模块每一级上采样都会融合低层的特征信息保证细节不丢失。第四层是堆叠结构。Flownet2 把多个子网络串联起来第一个网络输出初始光流第二个网络拿初始光流和原图一起作为输入输出残差修正。这个堆叠结构是 Flownet2 相比 Flownet 1.0 最大的改进也是它能处理大位移的关键。3.3 输入输出的格式要求Flownet2 对输入图像的格式有明确要求。图像需要是RGB 三通道尺寸最好是64 的倍数因为网络里有多次下采样尺寸不是 64 的倍数会在上采样时出现尺寸不匹配。如果输入图像尺寸不满足要求需要先 resize 或者 pad。输入图像的数值范围也有讲究。Flownet2 的预处理里会把像素值从 [0, 255] 归一化到 [0, 1]然后再做均值减法。均值减法的参数是 ImageNet 的均值这个在源码里有定义。如果你不做归一化直接输入 [0, 255] 的像素值网络输出会完全乱掉。输出是一个双通道的浮点数组形状是(H, W, 2)分别代表 x 方向和 y 方向的位移。这个位移是相对于原图尺寸的单位是像素。比如输出值 (10, -5) 表示这个像素在 x 方向移动了 10 个像素y 方向移动了 -5 个像素向上移动。4. 完整推理流程与代码实现4.1 数据准备与预处理先准备两帧图像。我用的是一段行车记录仪的视频截取相邻两帧。图像尺寸是 1920x1080这个尺寸对 Flownet2 来说偏大直接推理显存会爆。所以需要先 resize 到一个合适的尺寸比如 512x256 或者 640x360。import cv2 import numpy as np import torch # 读取两帧图像 img1 cv2.imread(frame1.png) img2 cv2.imread(frame2.png) # 转换颜色空间 BGR - RGB img1 cv2.cvtColor(img1, cv2.COLOR_BGR2RGB) img2 cv2.cvtColor(img2, cv2.COLOR_BGR2RGB) # resize 到 64 的倍数 target_size (512, 256) # (width, height) img1 cv2.resize(img1, target_size) img2 cv2.resize(img2, target_size) # 归一化到 [0, 1] img1 img1.astype(np.float32) / 255.0 img2 img2.astype(np.float32) / 255.0 # 转成 tensor形状从 (H, W, C) 变成 (C, H, W) img1 torch.from_numpy(img1).permute(2, 0, 1).unsqueeze(0) img2 torch.from_numpy(img2).permute(2, 0, 1).unsqueeze(0) # 拼接成 (B, 6, H, W) 的输入 input_tensor torch.cat([img1, img2], dim1)这里有个细节Flownet2 的输入是两帧拼接在一起的 6 通道张量不是分开传两个输入。这个和很多其他光流网络不一样需要注意。4.2 模型加载与推理from networks import FlowNet2 # 初始化网络 net FlowNet2() net net.cuda() net.eval() # 加载权重 checkpoint torch.load(checkpoints/FlowNet2_checkpoint.pth.tar) net.load_state_dict(checkpoint[state_dict]) # 推理 input_tensor input_tensor.cuda() with torch.no_grad(): flow net(input_tensor) # flow 的形状是 (1, 2, H, W) flow flow.squeeze(0).permute(1, 2, 0).cpu().numpy()推理过程中有几个注意事项显存占用。Flownet2 的完整版本参数量很大推理时显存占用大概在 4-6GB取决于输入尺寸。如果显存不够可以改用 FlowNet2-CSS 或者 FlowNet2-S这两个版本显存占用小很多。推理速度。在 GTX 1080 Ti 上512x256 的输入大概需要 0.5-1 秒。如果用 CPU 推理可能要几十秒基本不可用。所以 GPU 是必须的。数值稳定性。有些场景下网络输出的光流值会非常大比如几百像素这通常是因为输入图像里有极端的大位移或者纹理缺失区域。可以在后处理里做一个截断把光流值限制在一个合理范围内。4.3 光流可视化光流场是双通道的直接看数值看不出什么需要可视化。最常用的可视化方法是Middlebury 颜色编码用颜色表示方向用饱和度表示大小。def flow_to_color(flow): # flow 形状 (H, W, 2) h, w flow.shape[:2] # 计算角度和大小 angle np.arctan2(flow[:,:,1], flow[:,:,0]) magnitude np.sqrt(flow[:,:,0]**2 flow[:,:,1]**2) # 归一化角度到 [0, 1] angle (angle np.pi) / (2 * np.pi) # 归一化大小到 [0, 1] magnitude magnitude / (magnitude.max() 1e-8) # 转成 HSV hsv np.zeros((h, w, 3), dtypenp.float32) hsv[:,:,0] angle * 180 hsv[:,:,1] 255 hsv[:,:,2] magnitude * 255 # HSV 转 RGB hsv hsv.astype(np.uint8) rgb cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB) return rgb这个可视化函数里色相Hue表示运动方向比如红色表示向右青色表示向左饱和度Saturation固定为 255明度Value表示运动大小越亮表示移动越大。这种编码方式在光流论文里是标准做法方便对比不同算法的结果。4.4 结果保存与后处理# 保存光流数据 np.save(flow.npy, flow) # 保存可视化结果 flow_color flow_to_color(flow) cv2.imwrite(flow_vis.png, cv2.cvtColor(flow_color, cv2.COLOR_RGB2BGR))保存光流数据时建议用.npy或者.flo格式。.flo是 Middlebury 标准格式很多光流处理工具都支持。.npy是 numpy 原生格式读取方便。后处理方面有几个常见的操作中值滤波。光流场里经常有一些孤立的噪声点可以用中值滤波去掉。但要注意中值滤波会模糊运动边界如果后续要做精细的运动分割这一步要慎用。双向一致性检查。分别计算从第一帧到第二帧的光流以及从第二帧到第一帧的光流然后检查两者是否一致。不一致的区域通常是遮挡区域可以标记出来。光流上采样。如果推理时 resize 了图像得到的光流需要上采样回原尺寸。上采样时要注意光流值也要按比例缩放不能直接插值。5. 常见问题与排查技巧实录5.1 编译与运行时的典型报错报错信息原因解决方法nvcc: command not foundCUDA 路径未加入环境变量把 CUDA 的 bin 目录加到 PATHerror: identifier AT_CHECK is undefinedPyTorch 版本不匹配把源码里的 AT_CHECK 改成 TORCH_CHECKRuntimeError: CUDA out of memory显存不足减小输入尺寸或换用轻量版网络Missing key(s) in state_dict权重文件和网络定义不匹配检查网络配置是否和权重版本对应invalid load key权重文件损坏重新下载并校验 MD5numpy.ndarray size changednumpy 版本和 PyTorch 不兼容降级 numpy 到 1.19.55.2 光流质量差的排查思路如果跑出来的光流图看起来乱七八糟可以按以下顺序排查第一步检查输入图像。两帧图像是否对齐如果图像本身有旋转或者缩放光流结果肯定不对。另外检查图像是否过曝或者过暗极端光照条件下网络表现会下降。第二步检查预处理。归一化做了吗均值减法做了吗输入尺寸是 64 的倍数吗这几个问题是最常见的低级错误。第三步检查权重加载。权重真的加载成功了吗可以打印一下网络第一层的权重看看是不是随机初始化的值。如果权重没加载成功网络输出就是随机的。第四步检查光流范围。打印一下光流的最大值和最小值如果最大值超过图像尺寸说明网络输出了异常值。这时候可以试试换用 FlowNet2-SD 或者 FlowNet2-CSS-ft-sd这两个版本对小位移更敏感输出更稳定。5.3 性能优化的实操心得输入尺寸的选择。Flownet2 的推理时间和输入尺寸基本是线性关系。512x256 大概 0.5 秒1024x512 就要 2 秒左右。如果对精度要求不是极致可以用 512x256 先跑一遍看看效果够不够用。半精度推理。PyTorch 支持 FP16 推理可以把显存占用减半速度提升 30% 左右。但 Flownet2 的自定义 CUDA 扩展对 FP16 支持不完善直接开 FP16 可能会报错。需要手动改一些源码把 FP16 相关的类型转换加上。批处理。如果有多对图像需要处理可以拼成一个 batch 一起推理。但要注意Flownet2 的显存占用和 batch size 成正比batch size 太大照样爆显存。模型剪枝。如果只需要处理特定场景比如只有小位移可以用 FlowNet2-S 或者 FlowNet2-SD这两个版本参数量小很多速度快好几倍精度损失在可接受范围内。提示Flownet2 的推理结果对输入图像的尺寸非常敏感。同样的场景resize 到不同尺寸光流值会不一样。这是因为网络是在特定尺度上训练的输入尺寸变了感受野对应的实际物理尺寸也变了。所以如果要做定量分析最好固定一个输入尺寸所有图像都 resize 到同一个尺寸。5.4 与其他光流算法的对比算法精度速度显存占用适用场景Farneback低极快无纹理丰富、小位移DIS中快无实时应用FlowNet2-S中快低快速原型验证FlowNet2-CSS高中中通用场景FlowNet2最高慢高精度优先RAFT最高慢高精度优先更新如果现在重新选型RAFT 是更好的选择精度比 Flownet2 高代码更简洁依赖更少。但 Flownet2 在 2021 年那个时间点是比较成熟的选择而且它的堆叠结构思想对理解光流网络很有帮助。6. 实际项目中的经验总结6.1 数据预处理的几个关键细节在实际项目里输入数据的质量直接决定光流结果的上限。我踩过的一个坑是视频帧的压缩伪影。行车记录仪的视频是 H.264 压缩的相邻帧之间有明显的块效应这些块效应会被网络当成真实的纹理导致光流在块边界处出现异常值。解决办法是在预处理阶段加一个轻度的去块滤波或者用高质量的视频源。另一个坑是时间戳对齐。如果两帧之间的时间间隔不均匀光流值就不能直接比较。比如第一对帧间隔 33ms第二对帧间隔 50ms同样的运动速度光流值会差 50%。所以在做定量分析之前一定要确认帧率是恒定的。6.2 光流后处理的实用技巧光流出来之后通常还需要做一些后处理才能用于下游任务。我常用的几个操作遮挡检测。用双向一致性检查把前向光流和后向光流不一致的区域标记为遮挡。这些区域的光流值不可信下游任务里可以忽略。运动分割。对光流场做聚类把运动模式相似的像素分到一组。这个在车辆检测、行人跟踪里很有用。简单的做法是用 K-Means 对光流向量聚类复杂一点可以用 Mean Shift 或者图割。光流累积。如果要做长时序的运动分析可以把多帧的光流累积起来得到每个像素的轨迹。累积的时候要注意误差累积问题时间越长误差越大通常累积 5-10 帧就要重新初始化。6.3 什么场景下 Flownet2 会失效Flownet2 不是万能的以下几种场景它的表现会明显下降极端光照变化。比如从隧道出来突然遇到强光两帧之间的亮度差异巨大网络提取的特征完全对不上光流会乱掉。重复纹理。比如一面全是相同窗户的墙或者一片草地局部区域看起来都一样网络无法确定唯一的匹配关系光流会出现歧义。大面积的均匀区域。比如天空、白墙这些区域没有纹理网络只能靠周围的上下文推测推测结果往往不准。非刚性形变。比如一个人穿的衣服在风中飘动衣服的形变不是刚体运动光流估计会不准确。遇到这些场景可以考虑用 RAFT 或者 PWC-Net这两个网络在某些场景下比 Flownet2 更鲁棒。或者用传统方法和深度学习方法做融合取长补短。6.4 从 Flownet2 迁移到新方法的建议如果你现在刚开始做光流估计我建议直接上RAFT。RAFT 是 2020 年提出的精度比 Flownet2 高一大截代码更简洁依赖更少而且有官方维护的 PyTorch 实现。Flownet2 的很多设计比如堆叠结构在 RAFT 里被更优雅的迭代更新机制取代了。但 Flownet2 依然有学习价值。它的 Correlation 层设计、堆叠网络思想、多尺度特征融合策略这些都是光流估计的经典思路理解了这些再看 RAFT 或者其他新方法会容易很多。而且 Flownet2 的预训练权重在某些特定场景下比如小位移依然有优势可以作为 ensemble 的一部分。我个人在实际操作中的体会是光流估计这件事数据比模型重要预处理比后处理重要。与其花时间调网络结构不如先把输入图像的质量搞好把预处理做扎实。很多光流质量差的问题根源都在输入数据上而不是模型本身。