状态:草稿(2026-10-13),部分实测。 论文中的数值已对照原文及条件进行了确认。亲自尝试的内容为笔记本电脑 CPU 上的 numpy 示例(
code/05_flow_toy.py,不到 1 秒)。乒乓球视频的光流实测(Farneback)是代码库docs/tt/PERFORMANCE.md2.9.7 中的数值,RAFT 尚未在我们的视频中运行(未实测)。 系列《阅读视觉模型 2》第 5 篇。从这里开始探讨“运动”。下一篇: 视频模型.
目录
1. 直觉:每个像素一个箭头
重叠查看视频的两帧时,移动的部分会错开,静止的部分会重合。 光流是一张图,它为第一帧的每一个像素附上了一个箭头(水平移动 u,垂直移动 v),表示“它在下一帧去了哪里”。对于 1080p 的帧,大约有 200 万个箭头。
光流不说明“什么”在运动,只说明“哪里”以及“移动了多少”。因此,它在不认识物体的情况下也能使用。在乒乓球视频中,仅凭光流的大小就能很好地区分选手大幅移动的区间(拉力赛中)和徘徊的区间(拉力赛之间)。
2. 经典方法的假设及其局限
经典方法从两个假设出发。
- 亮度保持不变。 即使一个点移动了,该点的亮度也不会变:I₁(x, y) = I₂(x+u, y+v)。
- 运动很小。 因此可以对右侧进行一阶泰勒展开:I_x·u + I_y·v + I_t ≈ 0。
方程只有一个,但未知数有两个(u, v)。Lucas–Kanade(1981)假设“小窗口内的像素是一起运动的”,从而收集窗口内的方程并用最小二乘法求解。
这种方法在三个地方会失效。
- 孔径问题(aperture problem)。 在像条纹那样只有单方向图案变化的地方,沿着条纹的运动不会在亮度上留下任何痕迹。方程无法求解。
- 大运动。 一阶展开只有在运动为 1-2 个像素时才正确。如果像快球一样在一帧中移动几十个像素就会出错。先缩小图像使大运动变小、然后再放大的 金字塔是标准做法。
- 亮度发生变化的地方。 在阴影、反射和模糊中,第一个假设会破裂。乒乓球的模糊正好属于这种情况。
3. 学习模型的正确答案从何而来
无法在照片上手工标注每个像素的正确箭头。因此,基于学习的光流从 合成数据开始。FlowNet(2015)在照片背景上渲染并贴上了 809 种 3D 椅子模型,通过仿射变换移动,制作出了 Flying Chairs(22,872 对)。由于运动是人工制造的,正确答案是确切已知的(论文)。此后,延续到更加逼真的 FlyingThings3D、从动画电影中提取的 Sintel、以及汽车街景 KITTI 的训练顺序成为了标准。
损失函数通常是 端点误差(EPE, End-Point Error) 。它对预测箭头末端与真实箭头末端之间的距离(像素)求平均值。
4. 图解:RAFT
RAFT(Teed & Deng, ECCV 2020)由三个部分组成。
- 特征编码器。 将两帧输入同一个 CNN 中,提取 ⅛ 分辨率的特征(256维)。由于比较的是特征而不是亮度,“亮度保持不变”的假设就被放宽了。
- 所有像素对的相关体(all-pairs correlation volume)。 一次性计算第一帧所有位置与第二帧所有位置之间的内积,并将其置于 4 维表(H×W×H×W)中。然后,通过对后两个维度进行 1、2、4、8 步池化,构建 4 层金字塔。计算一次便大功告成。
- 迭代更新器。 在 4 层金字塔中查找当前光流估计所指示位置周围半径 4 格的所有内容,将这些值输入卷积 GRU 中,得出“应修正估计多少”(Δ)。在训练时重复使用相同权重的 GRU 12 次,在评估时在 Sintel 上重复 32 次、在 KITTI 上重复 24 次。论文记录称,即使运行多达 200 次也不会发散。
与以往将金字塔用作“从粗糙分辨率求解并向下细化到精细分辨率”的分阶段结构(如 PWC-Net 等)不同,RAFT 从单一的 ⅛ 分辨率就已经知道了所有距离的像素对,因此从一开始就能看到快速大幅移动的物体。
5. 如何进行训练
L = Σᵢ γ^(N−i) · ‖ fᵢ − f_정답 ‖₁, γ = 0.8, N = 반복 횟수
对所有迭代的输出施加损失,越是后面的迭代权重越大(论文)。这意味着初期的迭代可以犯大错,但必须逐渐变得正确。由于这种损失,GRU 学到的不是“一次性猜对”,而是“一点点修正”。
当把 ⅛ 分辨率的光流放大到原始分辨率时,不使用简单的插值,而是用网络学到的权重计算相邻 3×3 格的加权平均值(凸组合上采样)。这是为了防止光流在物体边界处模糊。
训练顺序是从头开始(无预训练):Flying Chairs 10 万步 → FlyingThings 10 万步 → 混合 Sintel、KITTI 等 10 万步 → KITTI 5 万步,并使用两张桌面级 GPU 进行训练(论文)。
6. 动手尝试
code/05_flow_toy.py 仅使用 numpy 模拟了 RAFT 的两种思路。
- 将带有平滑纹理的 64×64 图像平移 (3.4, −2.2) 像素来创建第二帧(由于采用傅里叶平移,小数点后的移动也很精确)。
- 以每个像素周围 5×5 的图块作为特征,一次性计算两帧 所有像素对的相似度,形成一个 4,096×4,096 = 1,680 万格的表格(相关体)。
- 在出发点 (0, 0) 周围半径 8 格的范围内挑选最相似的位置(查找)。精度仅达到整数单位。
- 从该估计出发,重复“按照当前估计将第二帧复原,并用剩余的亮度差除以梯度进行修正”。RAFT 的 GRU 通过 学习 来完成的工作,这里用 Lucas–Kanade 方程的 手写 规则代替了。
[무늬 있는 그림] 상관 부피 4,096×4,096 = 16.8M 칸
반경 안 argmax(정수) 추정: EPE 평균 0.6546px · 중앙 0.4472px, 가장 흔한 값 (3, -2)
갱신 1회: EPE 평균 0.1649px · 중앙 0.0414px
갱신 2회: EPE 평균 0.1726px · 중앙 0.0602px
갱신 5회: EPE 평균 0.1593px · 중앙 0.0499px
仅通过比较所有像素对只能达到整数 (3, −2),误差中位数为 0.45px(真实值 3.4, −2.2 与整数之间的距离)。经过一次更新后降到了 0.04px。之后没有继续减小的原因是手写规则的局限性(复原时使用的双线性插值的误差)。这就是 RAFT 训练 GRU 的原因。如果从数据而不是规则中学习“修正多少”,就能把插值误差或遮挡等情况也考虑进去。
我们在竖条纹上尝试了同样的操作。
[세로줄(구경 문제)]
반경 안 argmax(정수) 추정: EPE 평균 7.5758px, 가장 흔한 값 (-4, 0)
갱신 1–5회: 변화 없음 (식이 풀리지 않는 화소 100.0%)
→ 가로 움직임 중앙값 -4.00(참값 3.4), 세로 움직임은 17가지 값으로 흩어짐
由于竖条纹上下移动时看起来完全一样,因此无法知晓垂直方向的运动(分散在 17 个值中)。水平方向由于条纹间距为 8 个像素,3.4 和 3.4−8 = −4.6 无法区分(选择了最近的整数 −4)。更新方程根本无法求解。这就是用数字观察到的孔径问题。在这种情况下,学习模型也只能根据周围的上下文(物体边界、其他部分的运动)进行猜测。
7. 论文数值
| 模型 | 数值(论文) | 条件 |
|---|---|---|
| FlowNetS / FlowNetC(2015) | Sintel final 测试 EPE 7.42 / 7.28 | Flying Chairs 训练。在桌面级 GPU 上 0.08 / 0.15 秒 |
| PWC-Net(2018) | Sintel final 5.04, KITTI-15 Fl-all 9.60%, 参数量 875 万 | 在 1024×436 下约 35fps(桌面级 GPU) |
| RAFT(2020) | Sintel clean/final 1.94/3.18(两帧), 1.61/2.86(以前向光流初始化), KITTI-15 Fl-all 5.10% | 参数量 530 万(小版本 100 万)。在 1088×436 下 10 次迭代 0.10 秒(桌面级 GPU),1080p 12 次 550ms |
| RAFT,仅用合成数据 | Sintel 训练集 clean 1.43 · final 2.71, KITTI-15 EPE 5.04 · Fl-all 17.4% | 仅训练 Chairs → Things 并在其他数据上直接应用 |
| SEA-RAFT(2024) | Spring 基准测试 1px 错误率 3.686, EPE 0.363 | 迭代 4 次(训练)· 4–12 次(推理),在 540×960 下 RAFT 140.7ms 对比 70.96ms(桌面级 GPU) |
Fl-all 是端点误差同时超过 3 个像素且超过真实大小 5% 的像素比例。越低越好。
SEA-RAFT 将 RAFT 的损失改为了“两个拉普拉斯分布混合的负对数似然”。对于容易猜对的像素像 L1 一样处理,而对于本来就很模糊的像素(遮挡、孔径问题),则让网络自身输出不确定度,以减少误差惩罚。它直接回归初始估计而不是从 0 开始,从而减少了迭代次数(论文)。
8. 局限性
- 计算量大。 RAFT 处理一帧 1080p 大约需要 0.5 秒(桌面级 GPU,论文)。这与 60fps 的单帧预算(16.7ms)不在同一个数量级。
- 对小而快的物体较弱。 在 ⅛ 分辨率下,直径仅有几个像素的乒乓球比一个特征格还要小。对于球的运动,连接物体检测结果比光流更准确(第 9 篇)。
- 模糊会破坏亮度假设。 模糊的球在每帧中的形状都在变化,很难找到“同一个点”。
9. 与我们项目的联系
- 实测:作为一种廉价且粗糙的信号,它是可用的。 在 OpenTTGames 测试的 7 场比赛(51,060 帧)中,我们使用缩减至 160×90 分辨率的视频的 Farneback 光流(经典方法),以 1 秒为单位判断“当前是否在进行多拍对打(Rally)”。运动员区域光流的 AUC 为 0.798,视频整体帧差分法为 0.770,BlurBall 球热力图最大值为 0.860,如果将球和光流按秩平均(Rank Average)进行组合,其结果为 0.876 ,在 CPU 上每帧耗时 2.5–2.8ms(仓库
docs/tt/PERFORMANCE.md2.9.7)。这意味着当球被遮挡时,它可以作为辅助信号来提示“仍在进行多拍对打”。 - 弹跳判定尚无依据。 在将相同的光流和差分值简单拼接输入给学习型弹跳判定器(第 4 篇)的初期测试中,没有看到明显的收益,目前尚无定论(截至 2026-10-13 研究主管,研究进行中——主管的代码库尚在外部)。由于目前仅尝试了“拼接”这种结合方式,其他结合方案仍然开放。
- 球追踪器中的运动。 第 9 篇中的 TrackNetV4 和 V5 用 帧差分代替光流作为注意力图来突出显示运动中的球,而 TOTNet 将光流添加到输入中的版本在遮挡区间更准确,但相应的速度也更慢(第 9 篇)。
小蒂提示。 如果条纹衬衫上下移动,计算机并不知道它移动了。真的,我们通过示例亲自验证过。因此,光流用于“大致了解哪里移动了多少”,而球则需要单独寻找。
参考文献
- Lucas & Kanade, An Iterative Image Registration Technique with an Application to Stereo Vision, IJCAI 1981.
- Farnebäck, Two-Frame Motion Estimation Based on Polynomial Expansion, SCIA 2003. https://doi.org/10.1007/3-540-45103-X_50
- Dosovitskiy et al., FlowNet: Learning Optical Flow with Convolutional Networks, ICCV 2015. https://arxiv.org/abs/1504.06852
- Sun et al., PWC-Net: CNNs for Optical Flow Using Pyramid, Warping, and Cost Volume, CVPR 2018. https://arxiv.org/abs/1709.02371
- Teed & Deng, RAFT: Recurrent All-Pairs Field Transforms for Optical Flow, ECCV 2020. https://arxiv.org/abs/2003.12039
- Wang, Lipson, Deng, SEA-RAFT: Simple, Efficient, Accurate RAFT for Optical Flow, ECCV 2024. https://arxiv.org/abs/2405.14793
- 我们的测量:仓库
docs/tt/PERFORMANCE.md2.9.7。示例代码与输出:code/05_flow_toy.py,code/05_flow_toy.out.txt