一、光流估计核心概念
1. 什么是光流?
光流是空间运动物体在观测成像平面上的像素运动 "瞬时速度" ,它描述了图像序列中像素点随时间的运动变化。通过分析这些速度矢量,我们可以实现动态场景分析,比如目标跟踪、行为识别、视频 stabilization 等。
2. 光流估计的三大前提假设
光流算法的有效性依赖于以下三个核心假设:
- 亮度恒定 :同一点在不同帧中的亮度保持不变,即

- 小运动:时间变化不会引起位置剧烈变化,仅能通过前后帧灰度变化近似偏导数
- 空间一致:场景中邻近点在图像上也是邻近点,且运动速度一致(用于解决单方程多未知数问题)
3. 光流基本方程推导
由亮度恒定假设展开泰勒级数并忽略高阶项,可得光流约束方程:

其中:
:图像在 x,y 方向的梯度
:像素在 x,y 方向的运动速度
:图像随时间的变化梯度
二、Lucas-Kanade 光流算法原理
Lucas-Kanade 算法是稀疏光流的代表,仅跟踪图像中特征明显的点(如角点),效率更高、更适合实时场景。
核心思想
利用空间一致假设,对每个特征点周围的局部窗口(如 15×15)建立超定方程组,通过最小二乘法求解 u,v,解决单个约束方程无法求解两个未知数的问题。
金字塔光流优化
为了处理大运动 场景,算法引入图像金字塔:
- 先在低分辨率(高层金字塔)跟踪大位移
- 逐步向高分辨率(底层金字塔)细化,最终得到精确光流
完整代码
python
import numpy as np
import cv2
# 打开视频文件(也可改为0调用摄像头)
cap = cv2.VideoCapture('test.avi')
# 随机生成100种颜色,用于绘制不同特征点的运动轨迹
color = np.random.randint(0, 255, (100, 3))
# 读取第一帧并转为灰度图
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
# ---------------------- 1. 特征点检测参数 ----------------------
feature_params = dict(
maxCorners=100, # 最多检测100个角点
qualityLevel=0.3, # 角点质量阈值(保留质量前30%的点)
minDistance=7 # 角点之间最小距离,避免特征点过于密集
)
# 检测Shi-Tomasi角点作为待跟踪的特征点
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
# 创建与帧大小相同的掩模,用于绘制轨迹
mask = np.zeros_like(old_frame)
# ---------------------- 2. Lucas-Kanade光流参数 ----------------------
lk_params = dict(
winSize=(15, 15), # 局部搜索窗口大小
maxLevel=2 # 金字塔层数(0表示只用原始图像)
)
# ---------------------- 3. 主循环:逐帧计算光流 ----------------------
while True:
ret, frame = cap.read()
if not ret:
break
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 计算光流:获取特征点在当前帧的新位置
p1, st, err = cv2.calcOpticalFlowPyrLK(
old_gray, frame_gray, p0, None, **lk_params
)
# 筛选成功跟踪的特征点(st=1表示跟踪成功)
good_new = p1[st == 1]
good_old = p0[st == 1]
# ---------------------- 4. 绘制运动轨迹 ----------------------
for i, (new, old) in enumerate(zip(good_new, good_old)):
a, b = new.ravel() # 新点坐标
c, d = old.ravel() # 旧点坐标
a, b, c, d = int(a), int(b), int(c), int(d)
# 在掩模上绘制轨迹线段
mask = cv2.line(mask, (a, b), (c, d), color[i].tolist(), 2)
# 在当前帧上标记新特征点
frame = cv2.circle(frame, (a, b), 3, color[i].tolist(), -1)
# 叠加掩模与当前帧,显示最终结果
img = cv2.add(frame, mask)
cv2.imshow('Lucas-Kanade Optical Flow', img)
# 按ESC键退出
k = cv2.waitKey(150) & 0xff
if k == 27:
break
# ---------------------- 5. 更新状态,准备下一帧 ----------------------
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1, 1, 2) # 重塑为OpenCV要求的格式
# 释放资源
cap.release()
cv2.destroyAllWindows()
代码详解
python
# 随机生成100种颜色,用于绘制不同特征点的运动轨迹
color = np.random.randint(0, 255, (100, 3))
# 读取第一帧并转为灰度图
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
生成 100 行 3 列的随机颜色数组(RGB),每个特征点用不同颜色画轨迹,方便区分。
- 读取视频第一帧
- 转为灰度图(光流算法只需要灰度信息,速度更快)
old_gray保存为 "上一帧",用于后续对比计算运动
python
# ---------------------- 1. 特征点检测参数 ----------------------
feature_params = dict(
maxCorners=100, # 最多检测100个角点
qualityLevel=0.3, # 角点质量阈值(保留质量前30%的点)
minDistance=7 # 角点之间最小距离,避免特征点过于密集
)
# 检测Shi-Tomasi角点作为待跟踪的特征点
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
这一步是找要跟踪的点。
- 使用
goodFeaturesToTrack检测角点(边缘交点,最适合跟踪) maxCorners=100:最多跟踪 100 个点p0存储第一帧所有要跟踪的特征点坐标
python
# 创建与帧大小相同的掩模,用于绘制轨迹
mask = np.zeros_like(old_frame)
创建一张和视频帧一样大的全黑图片 mask 。作用:在上面画轨迹,不会覆盖原视频画面。
python
# ---------------------- 2. Lucas-Kanade光流参数 ----------------------
lk_params = dict(
winSize=(15, 15), # 局部搜索窗口大小
maxLevel=2 # 金字塔层数(0表示只用原始图像)
)
金字塔 LK 光流参数:
winSize=(15,15):以点为中心,搜索周围 15×15 区域maxLevel=2:使用 2 层金字塔,能跟踪更快、更大的运动
python
# ---------------------- 3. 主循环:逐帧计算光流 ----------------------
while True:
ret, frame = cap.read()
if not ret:
break
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
循环读取每一帧:
ret:是否读到帧frame:当前彩色图frame_gray:当前灰度图(用于光流计算)
python
# 计算光流:获取特征点在当前帧的新位置
p1, st, err = cv2.calcOpticalFlowPyrLK(
old_gray, frame_gray, p0, None, **lk_params
)
输入:上一帧、当前帧、上一帧特征点输出:
p1:当前帧点的新位置st:状态(1 = 跟踪成功,0 = 丢失)err:误差
python
# 筛选成功跟踪的特征点(st=1表示跟踪成功)
good_new = p1[st == 1]
good_old = p0[st == 1]
只保留跟踪成功的点,丢掉丢失的点。
good_new:当前帧成功点good_old:上一帧成功点
python
# ---------------------- 4. 绘制运动轨迹 ----------------------
for i, (new, old) in enumerate(zip(good_new, good_old)):
a, b = new.ravel() # 新点坐标
c, d = old.ravel() # 旧点坐标
a, b, c, d = int(a), int(b), int(c), int(d)
# 在掩模上绘制轨迹线段
mask = cv2.line(mask, (a, b), (c, d), color[i].tolist(), 2)
# 在当前帧上标记新特征点
frame = cv2.circle(frame, (a, b), 3, color[i].tolist(), -1)
遍历每一对新旧点,画线 + 画点:
cv2.line:在 mask 上画运动轨迹线cv2.circle:在当前帧画当前特征点- 每个点用不同颜色,轨迹更清晰
python
# 叠加掩模与当前帧,显示最终结果
img = cv2.add(frame, mask)
cv2.imshow('Lucas-Kanade Optical Flow', img)
将原视频 和轨迹 mask叠加在一起显示。效果:视频上保留彩色运动轨迹。
结果展示:
