前言
- 最近有个无人机项目,使用光流传感器对无人机速度进行估计,用于飞控进行速度闭环。但是在测试期间出现了光流传感器在纯色地板上速度估计失效的现象。
- 本文将从光流的原理出发,使用
opencv-python对测试视频进行模拟光流传感器的速度估计算法,并对结果进行可视化,效果如下:
1 光流
1-1 什么是光流
光流(Optical Flow)就是用来描述"图像中像素在连续两帧之间的运动变化"的方法。- 通常我们可以用一堆箭头来表示光流的方向

1-2 光流能做什么?
- 光流 = 用图像变化来推断"世界在怎么动"或"相机在怎么动"
- 那么当然就可以运用到很多领域
| 应用 | 光流作用 |
|---|---|
| 无人机 | 估计速度 / 悬停 |
| 机器人 | 避障 |
| 监控 | 运动检测 |
| 视觉跟踪 | 目标追踪 |
| AR/VR | 稳定画面 |
1-3 光流有两大类
1-3-1 稀疏光流(Sparse)
- 只跟踪"特征点",比如:
- 角点
- IR点阵
- 代表方法:
- Lucas-Kanade
- 比如说下图的车灯

1-3-2 稠密光流(Dense)
- 每个像素都算运动
- 代表方法:
- Farneback
- DeepFlow
- 将会对每一个像素都进行估计

2 Farneback原理
2-1 介绍
- Farneback本质是稠密光流估计的一种算法。
- 其核心就是用"局部多项式拟合"来估计整张图像运动。
2-2 核心原理
- Farneback 光流 = 用局部多项式拟合图像结构,再比较连续帧的偏移,从而得到"整张图的运动场"。
- 其中局部用"二次多项式表示图像":意思是在一个小窗口内:I(x,y)≈ax2+by2+cxy+dx+ey+f I(x,y)\approx ax^2 + by^2 + cxy + dx + ey + fI(x,y)≈ax2+by2+cxy+dx+ey+f也就是说图像的每个局部区域都用一个"平滑曲面"表示。(因为真实图像在小范围内"变化是连续的",可以用一个简单函数近似它)
- 然后比较两帧"曲面如何移动",找到dxdxdx与dydydy使曲面f(x,y)f(x,y)f(x,y)与f(x+dx,y+dy)f(x + dx, y + dy)f(x+dx,y+dy)两者最接近
- 最终算法将输出flowy,x=(dx,dy)flowy, x = (dx, dy)flowy,x=(dx,dy)也就是每个像素的运动向量。
2-3 Farneback算法流程
- 图像预处理(构建金字塔):输入两帧图像,然后构建多尺度图像金字塔,目的是为了从低分辨率到高分辨率逐层估计运动
- 局部多项式建模:在每个像素邻域内,假设图像可以表示为:I(x,y)≈ax2+by2+cxy+dx+ey+f I(x,y)\approx ax^2 + by^2 + cxy + dx + ey + fI(x,y)≈ax2+by2+cxy+dx+ey+f也就是用一个二次曲面拟合局部灰度变化
- 帧间模型位移假设:假设第二帧是第一帧的平移版本It+1(x,y)=It(x+dx,y+dy)I_{t+1}(x,y)=I_t(x+dx,y+dy)It+1(x,y)=It(x+dx,y+dy)求每个位置的(dx,dy)(dx, dy)(dx,dy)
- 多项式对齐(核心):对比两帧局部模型,通过数学优化,找到 dxdxdx, dydydy,使两个局部多项式误差最小∣∣Pt(x,y)−Pt+1(x+dx,y+dy)∣∣|| P_t(x,y) - P_{t+1}(x+dx,y+dy) ||∣∣Pt(x,y)−Pt+1(x+dx,y+dy)∣∣其中 dxdxdx, dydydy即为每个像素的运动向量
- 金字塔迭代,先估计大位移,上采样结果,在高分辨率修正
- 最终算法会输出一个向量场flow(x,y)=(dx,dy)flow(x,y) = (dx, dy)flow(x,y)=(dx,dy)即每个像素都有运动方向
2-4 为什么Farneback只关注灰度信息?
- 光流只需要"亮度变化",不需要"颜色信息"。
- 光流的基本假设是:
同一个点在短时间内亮度不变
- 也就是:I(x,y,t)=I(x+dx,y+dy,t+Δt)I(x,y,t)=I(x+dx,y+dy,t+\Delta t)I(x,y,t)=I(x+dx,y+dy,t+Δt)
- 关键变量只有一个:
- I = 亮度(Intensity)
- 而且光流关心的是边缘、纹理、明暗变化这些东东, 灰度刚好保留这些
2-5 OpenCV的Farneback实现
OpenCV中内置实现了Farneback算法
python
cv2.calcOpticalFlowFarneback(
prev, next, flow,
pyr_scale,
levels,
winsize,
iterations,
poly_n,
poly_sigma,
flags
)
- 算法输入:(注意必须是
同尺寸的灰度图)prev:上一帧next:当前帧
- 算法输出:
flow,即:每个像素的运动向量flowy,x=(dx,dy)flowy, x = (dx, dy)flowy,x=(dx,dy)
2-5-1 核心参数:
pyr_scale:金字塔缩放比例, 每一层图像缩小多少,用来处理"大位移"- 默认为
0.5,数值越小越稳定但是越慢,数值越大越快但是越不准
- 默认为
levels:金字塔层数,就是图像要缩几层winsize:窗口大小,就是每次看多大区域来判断运动- 越小细节越多但更容易抖
iterations:迭代次数,每一层算几次优化poly_n:多项式窗口大小,用多大邻域来拟合"曲面",就是上面那个I(x,y)≈ax2+by2+cxy+dx+ey+f I(x,y)\approx ax^2 + by^2 + cxy + dx + ey + fI(x,y)≈ax2+by2+cxy+dx+ey+fpoly_sigma:平滑程度,曲面拟合的"平滑程度"- 越小更敏感,但噪声多。越大 更平滑,但细节少
flags:模式开关flags = 0:使用高斯权重(更平滑)
3 代码实现
3-1 核心目标
- 我们要做的事情非常简单,读取一段视频,借助
OpenCV内置实现的Farneback算法,计算出连续视频帧的像素向量场,并将其可视化为箭头的同时计算整体平均速度。、
3-2 完整代码
- 老规矩先给出全部代码,再细致分析
python
import cv2
import numpy as np
VIDEO_PATH = "test02.mp4"
cap = cv2.VideoCapture(VIDEO_PATH)
if not cap.isOpened():
raise RuntimeError("视频打开失败")
ret, frame = cap.read()
if not ret:
raise RuntimeError("读取失败")
prev_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
scale = 0.5 # 降采样提升稳定性
while True:
ret, frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 可选降采样
gray_s = cv2.resize(gray, None, fx=scale, fy=scale)
prev_s = cv2.resize(prev_gray, None, fx=scale, fy=scale)
flow = cv2.calcOpticalFlowFarneback(
prev_s,
gray_s,
None,
pyr_scale=0.5,
levels=3,
winsize=15,
iterations=3,
poly_n=5,
poly_sigma=1.2,
flags=0
)
# flow[...,0] = dx, flow[...,1] = dy
fx = flow[..., 0]
fy = flow[..., 1]
# 计算平均运动(核心)
mean_fx = np.mean(fx)
mean_fy = np.mean(fy)
vis = frame.copy()
h, w = vis.shape[:2]
step = 20
for y in range(0, h, step):
for x in range(0, w, step):
dx = fx[int(y * scale), int(x * scale)]
dy = fy[int(y * scale), int(x * scale)]
cv2.arrowedLine(
vis,
(x, y),
(int(x + dx * 5), int(y + dy * 5)),
(0, 255, 0),
1,
tipLength=0.3
)
text = f"Flow X: {mean_fx:.3f} Y: {mean_fy:.3f}"
cv2.putText(vis, text, (20, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8,
(0, 0, 255), 2)
print(text)
cv2.imshow("PX4FLOW-like Optical Flow", vis)
prev_gray = gray.copy()
key = cv2.waitKey(1)
if key == 27:
break
cap.release()
cv2.destroyAllWindows()
3-3 读取视频
python
VIDEO_PATH = "test02.mp4"
cap = cv2.VideoCapture(VIDEO_PATH)
ret, frame = cap.read()
prev_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
- 打开视频流并转灰度图
3-4 降采样
python
scale = 0.5
gray_s = cv2.resize(gray, None, fx=scale, fy=scale)
prev_s = cv2.resize(prev_gray, None, fx=scale, fy=scale)
- 这里我们把图缩小一半,加速计算而且可以一定程度上降噪提高稳定性。
3-5 核心:计算光流
python
flow = cv2.calcOpticalFlowFarneback(
prev_s,
gray_s,
None,
pyr_scale=0.5,
levels=3,
winsize=15,
iterations=3,
poly_n=5,
poly_sigma=1.2,
flags=0
)
- 请见
2-5 OpenCV的Farneback实现查看具体参数信息 - 算法输出二维向量场:每个像素从上一帧到当前帧移动了多少
python
flow[y, x] = (dx, dy)
3-6 拆出 x/y 方向
python
fx = flow[..., 0]
fy = flow[..., 1]
- fx = 水平运动(左右)
- fy = 垂直运动(上下)
3-7 平均速度估计
python
mean_fx = np.mean(fx)
mean_fy = np.mean(fy)
- 直接取平均值
3-8 可视化向量场
python
step = 20
for y in range(0, h, step):
for x in range(0, w, step):
dx = fx[int(y * scale), int(x * scale)]
dy = fy[int(y * scale), int(x * scale)]
cv2.arrowedLine(
vis,
(x, y),
(int(x + dx * 5), int(y + dy * 5)),
(0, 255, 0),
1,
tipLength=0.3
)
- 为了保证可视化,我们不把每一个像素都画箭头,我们每隔20像素取一个点,取画图
- 注意因为我们缩放了图像,所以要乘
scale - 箭头的大小表示这个位置的运动速度有多大(位移有多强)
3-9 更新帧
python
prev_gray = gray.copy()
- 当前帧变上一帧,方便下一轮循环迭代
4 测试:
4-1 普通场景测试


4-2 纯色地板测试


4-3 小结
- 可以看到,纯色地板压根就没有什么箭头
- 这是因为纯色地板图像灰度几乎恒定,缺乏可用于匹配的特征结构,局部多项式拟合退化。
- 我们回顾以下光流依赖的基本假设:
- 图像亮度在短时间内保持不变,并且空间上具有可区分结构
- 那么在纯色区域
- 亮度不变(满足)
- 但空间结构不存在(关键失败点)
总结
- 本文基于Optical flow原理与Farneback算法实现,验证了光流在存在纹理时可稳定估计运动,但在低纹理(纯色)场景下因空间结构缺失导致估计退化甚至失效。
- 如有错误!欢迎指出!
- 感谢观看!