正月十五,元宵花灯游行的队伍从桥下缓缓经过。你站在桥上,第一眼记住了一盏莲花灯的位置,下一秒又在稍远的地方找到它。整个过程你没有数步子,也没有拿尺子,只是用前后两张画面,心里就冒出一个词:它往右走了三步。计算机视觉里的光流估计,做的就是这件事:给定相邻两帧图像,为每个像素判断它从上一帧到这一帧移动到了哪里、移动了多远,最后得到一张铺满整幅图的二维速度场。这听起来像在追踪花灯,但它不关心花灯是谁,只关心“花灯这个纹理点”往哪走、走多快。这篇文章会用看花灯游行这个场景,把光流的数学定义、亮度恒定假设、孔径问题、Lucas-Kanade、金字塔策略、深度学习方法以及实际应用串起来。学完之后你会发现,机器看视频时并不是真“看懂”了,而是先把每个像素的“脚步”算清楚,再交给后面的任务使用。
光流是计算机视觉从视频中提取运动信息的地基。理解它的原理,能帮你正确选择传统方法与深度学习方法,也能让你在动作识别、自动驾驶等 AI 应用中看懂最核心的输入特征。
目录
可视化演示
展示光流向量场的不同运动模式(平移/旋转/扩展/复杂),对比Lucas-Kanade稀疏方法与深度学习稠密方法,支持模式切换与参数交互。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+captionField+'
光流概念定义:像素的“脚步”
花灯队伍是移动的,但灯与灯之间没有名字。你之所以能认出那盏莲花灯,是因为它身上的红色花瓣纹路在两步之间没有变化。你并不需要知道它叫“莲花灯”,只需要记住它刚才在桥洞左边,现在在桥洞右边,就能算出它往右走了几步。计算机里的图像没有“名字”的概念,只有一排排像素,因此光流估计做的事情更朴素:把每个像素看成一个小小的观察点,比较它在前后两帧里分别出现在哪里,然后算出它的运动方向和速度大小。
在数学上,我们给图像中的每一个像素位置 \((x,y)\) 分配一个二维矢量,记为光流矢量 \((u,v)\)。其中 \(u\) 表示像素在水平方向的移动速度,单位是“像素/帧”;\(v\) 表示像素在竖直方向的移动速度。把所有像素的速度拼在一起,就得到一张覆盖整幅图像的速度场。这张速度场既可以是稠密的——每个像素都有值;也可以是稀疏的——只在角点等明显特征处计算,后者在计算量受限时非常实用。
所以光流估计不是“认人”,而是“看脚步”。它不会告诉你这是莲花灯还是兔子灯,只会告诉你这两个时刻之间,每个纹理点往哪个方向挪动了。正因如此,它才能成为动作识别、自动驾驶和视频插帧等任务最底层的运动信号。
- 水平速度 u花灯每秒在图像 \(x\) 方向移动的像素数。
- 垂直速度 v花灯每秒在图像 \(y\) 方向移动的像素数。
- 光流向量把 \(u\) 和 \(v\) 合成一个矢量,就是这个像素的“脚步”。
亮度恒定假设:花灯花纹不变的约定
你在桥上辨认花灯,靠的是灯上独特的花纹。如果前一秒这盏灯还是红底金花,后一秒突然变成蓝底白条,你就不敢确定它还是原来那盏灯。光流估计也一样,它需要一个“认灯”的约定:同一个物理点在前后两帧里的灰度值应该保持不变。这就是亮度恒定假设。
用数学语言说,假设第 \(t\) 帧图像在位置 \((x,y)\) 的亮度是 \(I(x,y,t)\),经过很短的 \(dt\) 时间后,这个点移动到 \((x+dx, y+dy)\),亮度仍然近似为原来的值。对移动后的亮度做一阶泰勒展开,再除以时间 \(dt\),就能把“空间上的变化速度”和“时间上的亮度变化速度”联系起来,得到光流约束方程:
这个方程看起来很简洁,但它只有一个式子,却有两个未知数 \(u\) 和 \(v\)。也就是说,只看单个像素,我们无法唯一确定花灯往哪走。这就好像你在桥上只看到一盏纯红色花灯,既没有花纹也没有挂穗,你很难判断它是横着走还是竖着走。于是,下一步就要处理这个“信息不足”的问题。
- 水平空间梯度 Ix表示图像亮度在水平方向上变化得快还是慢。
- 垂直空间梯度 Iy表示图像亮度在竖直方向上变化得快还是慢。
- 时间梯度 It表示同一个像素位置上的亮度随时间变化的快慢。
- 约束方程空间梯度乘上运动速度,再加上时间梯度,结果应该等于零。
孔径问题:透过钥匙孔看花灯
想象你透过门上的钥匙孔看游行的花灯队伍。队伍里有一面巨大的条纹旗,条纹是竖着的。如果整面旗横着向左移动,你透过钥匙孔看到的画面几乎不变:条纹还是那些条纹,位置还是那些位置。你甚至会怀疑旗子根本没动。这就是孔径问题:当观察窗口太小时,我们只能看到局部纹理,而局部纹理沿着某个方向的重复性太强,导致那一个方向上的运动信息被隐藏了。
在数学上,当一个小窗口里的像素亮度只在水平方向有变化、在竖直方向没有变化时,\(I_y\) 近似为零,约束方程就退化成 \(I_x u + I_t = 0\)。这时我们能估计出的只有沿梯度方向的运动分量,也就是法向流,而沿边缘方向的切向运动几乎是完全不可知的。
因此,单靠一个像素或者一个过小的窗口,永远会被“条纹旗”骗到。解决办法有两个方向:一是把窗口变大,让窗口里出现更多方向的花纹,从而增加可用信息;二是加入全局平滑的约束,让相邻像素的运动彼此靠近。Lucas-Kanade 方法走的就是第一条路。
- 梯度向量把水平梯度和竖直梯度组成向量,代表花灯花纹变化最剧烈的方向。
- 法向速度用时间变化量除以梯度长度,并把结果放在梯度方向上,得到垂直于边缘的运动分量。
- 切向丢失沿着条纹方向没有亮度变化,所以那个方向上的运动无法从局部判断,这就是孔径问题的本质。
Lucas-Kanade:让邻居们一起投票
既然一盏花灯自己看不清方向,那就叫上它周围的邻居一起看。Lucas-Kanade 方法的核心假设是:在一个较小的窗口内,所有像素属于同一个运动物体,因此它们共享同一个速度 \((u,v)\)。窗口里每个像素都能写出一个亮度恒定方程,几十个像素合在一起,就能提供足够的约束来求解两个未知数。
把这些方程叠在一起,得到一个超定方程组:每一个像素贡献一行梯度 \([I_x, I_y]\),右边是负的时间梯度 \(-I_t\)。由于方程个数远多于未知数,不能要求每个方程都精确成立,只能求一个“大家都尽量满意”的解,这就是最小二乘。整理后得到正规方程:
换句话说,Lucas-Kanade 是“让邻居投票表决”。如果邻居们的花纹方向足够丰富,票数结构就稳定,结果可信;如果整片窗口都只有同一方向的条纹,投票就会一边倒,解就变得不稳定。因此实际使用中,人们常常只在角点这类“两个方向都有纹理”的位置计算稀疏光流。
- 邻域窗口取目标像素周围一小块区域,例如 \(5 \times 5\) 或 \(7 \times 7\),假设里面所有像素运动相同。
- 最小二乘方程个数多于未知数时,求使所有方程残差平方和最小的解。
- 正规方程对最小二乘目标求导并令导数为零,得到 \(A^{\mathrm{T}}A\) 形式的方程。
- 矩阵可逆性只有 \(A^{\mathrm{T}}A\) 的两个特征值都不接近零,解才是可信的;这也对应窗口里同时存在至少两个方向的花纹。
多尺度金字塔:站远一点看大花灯
花灯如果走得特别快,两帧之间一下子跳出了你的视线范围,你连它刚才在哪都找不到,更别说算速度了。这时候最自然的做法是站远一点、眯起眼睛看:距离拉远了,大位移在视野里就变成了小位移。图像金字塔正是模仿这个动作。先把图像一层层缩小,在最模糊、最小的那层上估算光流,再把结果一层层放大回去,作为下一层估计的初始值,不断修正。
最简单的图像金字塔构建方式是不断隔行隔列采样,让每一层图像的长和宽都变成上一层的一半。小图中物体的位移被等比缩小,原本 16 像素的大跳变成 2 像素的小跳,完全落在 Lucas-Kanade 的“小运动”假设范围内。
金字塔策略让传统方法在面对大位移时不再“两眼一抹黑”。它没有改变光流估计的基本方程,只是改变了观察花灯的“距离”。今天几乎所有经典光流算法都会内置金字塔,这也是“先看全局、再抠细节”的典型工程智慧。
- 金字塔层级第 \(k+1\) 层是第 \(k\) 层缩小一半后的图像。
- 坐标压缩\(2x, 2y\) 表示每隔一个像素取一个值,长宽各缩一半。
- 由粗到精先在最高层算大尺度位移,再逐层放大并作为初始值继续优化。
深度学习方法:经验丰富的老向导
前面所有方法都像是一套手工制定的规则:假设亮度不变、假设窗口内运动一致、假设先粗后精。可是现实视频里有光照突变、遮挡、模糊和复杂形变,这些规则很容易被打破。于是研究者换了一个思路:与其自己总结看花灯的经验,不如请一位看过成千上万次花灯游行的老向导。他不需要你告诉他“花纹要怎么匹配”,他只要看过大量双帧图像和对应的真实光流,就能自己学会运动模式。
深度学习方法从 FlowNet 开始,把光流估计变成一个有监督的回归问题:输入两张图像,卷积神经网络直接输出稠密光流场。后来的 RAFT 更进一步,用循环网络在多轮迭代中不断修正光流,同时利用相关性体素(correlation volume)来匹配两帧之间的特征块。和 Lucas-Kanade 相比,深度学习模型能捕捉更复杂的运动模式,对遮挡和大位移也更加鲁棒。训练时的损失函数通常就是预测光流与真值光流之间的误差:
深度学习的代价是需要大量带真值的训练数据。好在计算机图形学可以生成“会飞行的椅子”“会移动的物体”等合成视频,自动算出精确光流。老向导在合成世界里练就了一身本领,再迁移到真实世界,就能在实际视频中给出相当不错的运动估计。
- 预测光流网络对第 \(i\) 个像素输出的光流估计值。
- 真值光流数据集里人工标注或由合成渲染得到的标准答案。
- 平均误差对所有像素的预测误差求平均,作为训练时希望最小化的目标。
应用与评估:花灯队伍带来的价值
看完花灯游行之后,如果城市管理者想知道哪条路的人群流动最快、哪条路可能拥塞,他们不会只关心某一盏灯,而是会看整支队伍的速度场。光流在真实 AI 系统中扮演的正是这种角色。在自动驾驶中,摄像头两帧之间小物体横向移动产生的光流,可以帮助车辆区分自己是前进还是前面有障碍物逼近;在动作识别中,光流作为“运动通道”和 RGB 图像一起送入网络,能显著提升“跑步”和“挥手”这类动作的区分能力;在视频插帧中,利用光流把前一帧的像素搬运到中间位置,然后补出平滑的过渡帧。
光流并不总是正确的,所以还需要评估标准。最常用的指标是端点误差(EPE),它计算预测光流和真值光流之间的欧氏距离的平均值:
从 2D 图像的角度看,光流只是平面上的速度场;但从 3D 视觉的角度看,它是三维场景运动在相机平面上的投影。自动驾驶要判断“障碍物是否在靠近”,往往需要结合深度信息把 2D 光流还原成 3D 场景流。因此,光流从来不是终点,而是一系列高级视觉任务的起点。
- 分量误差预测水平速度与真值水平速度差,以及预测垂直速度与真值垂直速度差。
- 端点距离把两个方向误差用勾股定理合成一个距离。
- 平均对所有像素取平均,得到整张光流图的整体误差水平。
AI 应用场景
自动驾驶中的“移动花灯预警”
阶段:感知与运动估计
解决的问题:传统方法只检测静态物体,难以判断前方车辆是静止还是同向行驶。光流通过对相邻两帧图像计算稠密速度场,将背景的自身运动与前景物体的相对运动区分开,帮助判断碰撞风险。
具体案例:车载前置摄像头以 30 帧每秒采集画面,算法对每一对相邻帧计算稀疏光流。当画面中某个区域的光流方向与周围背景明显不一致,且速度持续增大时,系统判定有障碍物靠近并触发预警。
视频动作识别中的“人群节奏”
阶段:视频理解与特征提取
解决的问题:单独看一帧画面难以区分“跑步”和“走路”,因为姿态可能很相似。光流把像素运动变成运动场,让模型能看到“腿部向外摆动”的动态过程,从而提升动作识别准确率。
具体案例:在体育视频分析中,先对连续帧计算光流,再把光流图与 RGB 帧一起输入双流网络。某个“挥拍”动作在 RGB 帧里只表现为球拍位置变化,而光流图则清晰显示手臂向身体外侧甩动的速度方向。
视频插帧与慢动作生成
阶段:视频后处理与生成
解决的问题:低帧率视频在快速运动时会出现卡顿和抖动。光流估计出像素的移动路径后,可以沿着路径把像素“搬运”到中间时刻,生成自然流畅的中间帧。
具体案例:动画工作室把 24 帧每秒的原片补到 60 帧每秒。算法先估计相邻帧的光流,再按 0.5 的时间比例对光流场插值,最后把前一帧像素沿光流方向移动,合成过渡帧,大幅减少画面跳变。
Python 演示
import numpy as np def lucas_kanade(img1, img2, window_size=5): Ix = np.gradient(img1.astype(float), axis=1) Iy = np.gradient(img1.astype(float), axis=0) It = img2.astype(float) - img1.astype(float) h, w = img1.shape flow = np.zeros((h, w, 2)) r = window_size // 2 for y in range(r, h - r): for x in range(r, w - r): A = np.column_stack([ Ix[y-r:y+r+1, x-r:x+r+1].ravel(), Iy[y-r:y+r+1, x-r:x+r+1].ravel(), ]) b = -It[y-r:y+r+1, x-r:x+r+1].ravel() AtA = A.T @ A if np.linalg.cond(AtA) < 1e5: flow[y, x] = np.linalg.lstsq(AtA, A.T @ b, rcond=None)[0] return flow def make_pattern(h, w): yy, xx = np.mgrid[0:h, 0:w] return (np.sin(xx * 0.3) + np.cos(yy * 0.2) * 0.5) * 127 + 128 if __name__ == "__main__": img1 = make_pattern(64, 64) img2 = np.roll(np.roll(img1, 2, axis=1), 1, axis=0) flow = lucas_kanade(img1, img2) print("中心点光流 (u, v):", flow[32, 32].round(2))
Ix = np.gradient(img1.astype(float), axis=1)计算水平梯度,相当于观察花灯花纹在 x 方向的变化快慢。It = img2.astype(float) - img1.astype(float)计算前后帧亮度差,对应亮度恒定方程中的时间梯度。A = np.column_stack([...])把窗口内所有像素的梯度组成方程组的系数矩阵,每一行就是一位邻居“目击者”。AtA = A.T @ A把所有邻居的投票汇总成正规方程左侧的矩阵。if np.linalg.cond(AtA) < 1e5:检查矩阵是否病态,相当于判断窗口里是否只有单一方向的条纹。np.linalg.lstsq(AtA, A.T @ b, rcond=None)[0]用最小二乘解出最终的光流速度(u, v)。np.roll(np.roll(img1, 2, axis=1), 1, axis=0)生成向右移动 2 像素、向下移动 1 像素的第二帧,作为已知答案的测试输入。
常见误区
误区我以为光流估计就是把视频里的人和车框出来,然后一直跟着它。只要检测到目标,就沿着光流追下去。
正解光流不区分物体,它给每一个像素算一个二维速度,是底层运动场,不能回答“这是什么”。物体追踪通常需要目标检测、身份重识别和语义信息,而光流只是提供运动线索。好比你在桥上只记所有花灯的瞬间脚步,而不是跟在某一盏灯后面跑完整条街。
为何常见:因为光流和追踪都在研究“帧与帧之间的运动”,容易把“运动信息”和“语义跟踪”混为一谈。
误区既然叫亮度恒定,那拍视频时整段画面都不能有亮度变化,否则光流算法就不能用。
正解亮度恒定假设指的是同一个物理点在前后帧的灰度值不变,是为了建立“这盏花灯还是那盏花灯”的对应关系。它不等于整个画面的平均亮度不变。局部阴影、反光、全局曝光变化都会让这个假设失效,因此实际系统常需要对图像做归一化,或使用更鲁棒的损失函数。
为何常见:名字里的“亮度”太具有误导性,让人下意识把它理解成“视频整体亮度不变”。
误区既然 Lucas-Kanade 是最经典的光流方法,那把窗口调大一点,应该就什么运动都能估计出来。
正解Lucas-Kanade 依赖亮度方程的一阶泰勒展开,本质上是小位移假设。位移超过窗口范围时,局部匹配会失败,窗口再大也救不了线性化的误差。图像金字塔能把大位移缩小,但遮挡、重复纹理和光照突变仍然会让它失效。深度学习模型正是为了补上这些短板而出现的。
为何常见:因为教科书和小演示里给的都是“刚好移动一两像素”的例子,让人误以为算法天然适合所有尺度。
误区光流场里每个箭头都是物体在真实世界里的运动速度,直接拿来做 3D 导航就行了。
正解光流是三维运动在成像平面上的投影,受相机投影、景深、遮挡和光照影响。同一辆汽车,在侧面看和从正面看,同样速度产生的光流完全不同。真实三维运动需要结合深度估计或多视角几何,通常称为场景流。光流只是“从钥匙孔里看到的那部分真相”。
为何常见:因为光流场和真实速度场看起来都是箭头向量,很容易忽略相机投影造成的天然失真。
面试题
Q1. 光流估计最基本的输入是什么?
- A.单张静止图像
- B.相邻两帧图像← 正确答案
- C.一段完整音频
- D.三维点云数据
答案:B。光流估计需要比较前后两个时刻的图像,才能算出每个像素从哪移动到哪。单张图像没有时间变化,音频和点云都不是图像输入。
Q2. 亮度恒定假设最接近下列哪种日常经验?
- A.花灯颜色总是一模一样
- B.花灯花纹前后帧基本不变← 正确答案
- C.花灯队伍速度始终恒定
- D.花灯形状永远不会变化
答案:B。亮度恒定假设说的是同一个物理点在前后帧的灰度值近似不变,就像你靠花灯的花纹认出它是同一盏灯。它不要求速度恒定,也不要求形状永远不变。
Q3. 下列哪些任务会用到光流估计?
- A.自动驾驶障碍判断← 正确答案
- B.视频动作识别← 正确答案
- C.视频插帧补画面← 正确答案
- D.颜色直方图统计
答案:ABC。自动驾驶用光流判断障碍物相对运动,动作识别用光流提取运动特征,视频插帧用光流搬运像素生成中间帧。颜色直方图统计只描述颜色分布,与运动无关。
Q4. 孔径问题主要指的是:
- A.相机镜头孔径太小导致模糊
- B.局部单一方向使切向运动不可辨← 正确答案
- C.花灯被遮挡后无法匹配
- D.图像分辨率过低导致无法计算
答案:B。当局部窗口里只有单一方向的纹理,例如竖条纹,水平方向的移动看起来就像没动。这是观察窗口信息不足造成的,不是镜头硬件问题,也不是分辨率大小造成的。
Q5. Lucas-Kanade 方法的核心思路是:
- A.邻域内联合求最小二乘← 正确答案
- B.全图平滑加正则项
- C.用深度网络直接回归
- D.在频域做相位相关
答案:A。Lucas-Kanade 假设小窗口内所有像素共享同一个运动,然后把多个像素方程组合起来,用最小二乘求最优的 \((u,v)\)。全局平滑是 Horn-Schunck 的思想,频域相关也不是 LK 的做法。
Q6. 图像金字塔解决的主要问题是:
- A.亮度突变和阴影遮挡
- B.相邻帧之间的大位移← 正确答案
- C.纹理过于丰富多样
- D.图像存储空间过大
答案:B。金字塔通过逐层缩小图像,把大位移等比变成小位移,让 Lucas-Kanade 的小运动假设重新成立。它不解决光照变化,也不是为了节省存储空间。
Q7. 下列哪些因素会明显增加光流估计难度?
- A.光照突然变化← 正确答案
- B.物体运动幅度很大← 正确答案
- C.图像纹理丰富多样
- D.前后帧存在遮挡← 正确答案
答案:ABD。光照变化会破坏亮度恒定假设,大位移需要金字塔或深度模型处理,遮挡会让匹配点消失。纹理丰富反而是有利条件,因为它提供了更多方向的信息。
Q8. 当某邻域梯度方向几乎相同时,A^T A 会发生什么?
- A.矩阵变成全零矩阵
- B.至少一个特征值接近零← 正确答案
- C.矩阵变成单位矩阵
- D.行列式趋向正无穷
答案:B。梯度方向相同时,方程组高度相关,\(A^{\mathrm{T}}A\) 接近奇异,至少一个特征值接近零。这对应孔径问题,此时解出的光流在某个方向上非常不可靠。
Q9. 与 Lucas-Kanade 相比,深度学习光流的优势包括:
- A.端到端学习运动模式← 正确答案
- B.对遮挡和大位移更鲁棒← 正确答案
- C.完全不需要成对训练数据
- D.可以融合高层语义信息← 正确答案
答案:ABD。深度学习直接从数据中学习运动模式,对复杂场景有更强的拟合能力,也能利用语义信息。但它仍然需要大量带真值光流的成对训练数据,因此 C 不正确。
Q10. Lucas-Kanade 中矩阵 A^T A 的左上角元素等于:
- A.邻域内Ix与Iy乘积之和
- B.邻域内Ix的平方之和← 正确答案
- C.邻域内Iy的平方之和
- D.邻域内像素数量之和
答案:B。\(A^{\mathrm{T}}A\) 的 \((1,1)\) 位置是 \(A\) 第一列与自身的点积,也就是窗口内所有 \(I_x^2\) 之和。它是衡量水平梯度能量的关键值,与 \(I_y\) 或像素数量无关。