一、光流估计概念
光流(Optical Flow) 是计算机视觉中用于描述图像中像素点运动模式的技术。它基于以下两个基本假设:
亮度恒定假设:同一物体点在连续帧中的亮度保持不变
时空连续性假设:相邻点具有相似的运动
光流估计的核心思想是:在视频序列中,通过分析相邻帧之间像素强度的变化来估计像素的运动速度和方向。
二、函数详解
1. cv2.goodFeaturesToTrack() – Shi-Tomasi角点检测
用于在图像中检测角点(特征点),这些点适合进行光流跟踪。
参数说明:
p0 = cv2.goodFeaturesToTrack(image=old_gray, mask=None, **feature_params)
-
image:输入的单通道灰度图像
-
maxCorners=100:返回的最大角点数量(≤0表示无限制)
-
qualityLevel=0.3:角点质量阈值(0-1之间),值越大检测到的角点质量越高但数量越少
-
minDistance=7:角点之间的最小欧氏距离(像素),避免角点过于密集
-
mask=None:可选掩码,指定检测区域
-
blockSize=3(默认):计算角点时考虑的邻域大小
-
useHarrisDetector=False(默认):是否使用Harris角点检测器(False表示使用Shi-Tomasi)
返回值:
-
角点坐标数组,形状为(n, 1, 2)
2. cv2.calcOpticalFlowPyrLK() – Lucas-Kanade金字塔光流法
这是代码中核心的光流计算函数,使用金字塔Lucas-Kanade方法。
函数原型:
p1, st, err = cv2.calcOpticalFlowPyrLK(
prevImg=old_gray,
nextImg=frame_gray,
prevPts=p0,
nextPts=None,
**lk_params
)
参数详细说明:
输入参数:
-
prevImg:前一帧的灰度图像
-
nextImg:当前帧的灰度图像
-
prevPts:前一帧中需要跟踪的特征点坐标
-
nextPts:初始化为None,函数会计算并返回这些点在当前帧中的位置
# Lucas-Kanade光流法参数
lk_params = dict(
winSize=(15, 15), # 窗口大小
maxLevel=2, # 金字塔层数
# criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)
)
lk_params字典中的参数:
-
winSize=(15, 15):搜索窗口大小
-
较大的窗口可以处理更大的运动,但计算更慢
-
较小的窗口对快速运动敏感但可能丢失跟踪
-
-
maxLevel=2:金字塔层数
-
金字塔用于处理大位移运动
-
层数0表示不使用金字塔,只使用原始图像
-
每增加一层,图像尺寸减半,可以处理更大的运动
-
-
criteria(代码中已注释):迭代终止准则
-
例如:(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)
-
表示最大迭代次数10或变化小于0.03时停止
-
可选参数(代码中未使用):
-
flags:计算选项
-
0:普通方法
-
cv2.OPTFLOW_LK_GET_MIN_EIGENVALS:使用最小特征值
-
-
minEigThreshold:特征值阈值,用于判断跟踪质量
返回值:
-
p1:当前帧中估计的特征点位置
-
st:状态向量(与p0同大小)
-
1:成功跟踪
-
0:跟踪失败
-
-
err:误差向量,表示每个点的跟踪误差
三、算法流程解析
1. 初始化阶段
# 读取第一帧并转为灰度
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
# 检测角点作为特征点
p0 = cv2.goodFeaturesToTrack(old_gray, maxCorners=100, qualityLevel=0.3, minDistance=7)
2. 光流计算循环
p1, st, err = cv2.calcOpticalFlowPyrLK(prevImg=old_gray, nextImg=frame_gray,prevPts=p0,nextPts=None,**lk_params)
# 选择成功的点(状态为1的点)
good_new = p1[st == 1]
good_old = p0[st == 1]
# 绘制轨迹
for i, (new, old) in enumerate(zip(good_new, good_old)):
# 获取新点和旧点的坐标
a, b = new # 新点坐标
c, d = old # 旧点坐标
# 转换为整数
a, b, c, d = int(a), int(b), int(c), int(d)
# 在掩模上绘制线段,连接新点和旧点
mask = cv2.line(mask, (a, b), (c, d),
color[i].tolist(), thickness=2)
# 显示掩模
cv2.imshow('mask', mask)
# 将掩模添加到当前帧上,生成最终图像
img = cv2.add(frame, mask)
# 显示图像
cv2.imshow('frame', img)
# 等待30ms,检测是否按下了Esc键(键码为27)
key = cv2.waitKey(30)
if key == 27: # 按下Esc键,退出循环
break
# 更新灰度图和特征点
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1, 1, 2)
四、金字塔Lucas-Kanade算法原理
1. 基本Lucas-Kanade方程
对于每个特征点,假设在一个小的邻域窗口内,所有像素具有相同的运动:
I(x,y,t) = I(x+dx, y+dy, t+dt)
通过泰勒展开得到光流方程:
I_x * u + I_y * v + I_t = 0
其中:
-
I_x, I_y:图像在x,y方向的梯度
-
I_t:时间梯度
-
u, v:x,y方向的速度
2. 金字塔方法
-
构建图像金字塔:从原始图像开始,每层尺寸减半
-
从顶层开始计算:在低分辨率层计算粗略光流
-
逐层细化:将粗略结果作为下一层的初始值
-
最终在原始分辨率层得到精确结果
3. 窗口搜索
对于每个特征点,在其周围的winSize窗口内搜索最佳匹配位置。
五、应用场景
运动检测与分析
视频稳定
目标跟踪
动作识别
三维重建
自动驾驶(车辆/行人运动估计)
六、优缺点
优点:
-
计算效率高,适合实时应用
-
对亮度变化有一定鲁棒性
-
金字塔结构可以处理较大位移
局限性:
-
依赖于亮度恒定假设
-
对快速运动或遮挡处理不佳
-
需要好的特征点(角点)
七、实际运用
import cv2
import numpy as np
# 读取视频文件
cap = cv2.VideoCapture('renwushipin.avi')
# 读取第一帧
ret, old_frame = cap.read()
# 转换为灰度图像
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
# Shi-Tomasi角点检测参数
feature_params = dict(
maxCorners=100, # 最大角点数量
qualityLevel=0.3, # 角点质量的阈值
minDistance=7 # 最小距离,用于分散角点
)
# Shi-Tomasi角点检测
p0 = cv2.goodFeaturesToTrack(image=old_gray,mask=None,**feature_params)
# 创建用于绘制轨迹的掩模
mask = np.zeros_like(old_frame)
# Lucas-Kanade光流法参数
lk_params = dict(
winSize=(15, 15), # 窗口大小
maxLevel=2, # 金字塔层数
# criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)
)
# 生成随机颜色用于绘制轨迹
color = np.random.randint(0, 255, (100, 3))
while True:
# 读取下一帧
ret, frame = cap.read()
if not ret:
break
# 转换为灰度图
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 计算光流
p1, st, err = cv2.calcOpticalFlowPyrLK(prevImg=old_gray, nextImg=frame_gray,prevPts=p0,nextPts=None,**lk_params)
# 选择成功的点(状态为1的点)
good_new = p1[st == 1]
good_old = p0[st == 1]
# 绘制轨迹
for i, (new, old) in enumerate(zip(good_new, good_old)):
# 获取新点和旧点的坐标
a, b = new # 新点坐标
c, d = old # 旧点坐标
# 转换为整数
a, b, c, d = int(a), int(b), int(c), int(d)
# 在掩模上绘制线段,连接新点和旧点
mask = cv2.line(mask, (a, b), (c, d),
color[i].tolist(), thickness=2)
# 显示掩模
cv2.imshow('mask', mask)
# 将掩模添加到当前帧上,生成最终图像
img = cv2.add(frame, mask)
# 显示图像
cv2.imshow('frame', img)
# 等待30ms,检测是否按下了Esc键(键码为27)
key = cv2.waitKey(30)
if key == 27: # 按下Esc键,退出循环
break
# 更新灰度图和特征点
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1, 1, 2) # 重新整理特征点为适合下次计算的形状(n,2)->(n,1,2)
# 释放资源
cap.release()
cv2.destroyAllWindows()

这个代码示例完整展示了如何使用OpenCV实现基于特征点的光流跟踪,包括特征点检测、光流计算、轨迹绘制等完整流程。从代码众,可以看到,这个代码使用的是视频,和之前使用图片时的代码读取方式有所不同,那么有什么不同呢?
八、视频读取方法及与图片的区别
1.视频读取的详细机制
1) VideoCapture对象
# 创建视频捕获对象
cap = cv2.VideoCapture('renwushipin.avi')
# 或从摄像头读取
# cap = cv2.VideoCapture(0) # 0表示默认摄像头
关键属性和方法:
-
cap.isOpened(): 检查是否成功打开
-
cap.get(propId): 获取视频属性(帧率、尺寸等)
-
cap.set(propId, value): 设置视频属性
2)逐帧读取循环
while True:
# 读取一帧
ret, frame = cap.read()
# ret: 布尔值,表示是否成功读取
# frame: 当前帧的图像数据(numpy数组)
if not ret: # 视频结束或读取失败
break
# 处理当前帧(光流计算、显示等)
2. 对比
1)数据结构不同
# 视频读取 – 连续帧序列
cap = cv2.VideoCapture('video.avi')
ret, frame = cap.read() # 每次读取一帧
# 图片读取 – 单张图像
img = cv2.imread('image.jpg') # 一次性读取整个图像
如果想使用摄像头,只需要将cap = cv2.VideoCapture('video.avi')改为cap = cv2.VideoCapture(0)
2) 读取方式不同
# 视频 – 循环逐帧读取
while True:
ret, frame = cap.read() # 需要手动控制读取
if not ret: # 检查是否读取成功
break
# 处理每一帧
# 图片 – 一次性读取
img = cv2.imread('image.jpg') # 单次调用读取所有数据
3.在光流估计中的特殊意义
1) 时间连续性要求
# 光流需要连续帧的时间序列
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
while True:
ret, frame = cap.read()
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 计算相邻帧之间的运动
p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
# 更新为下一帧准备
old_gray = frame_gray.copy()
2) 特征点追踪的持续性
# 需要保持特征点的连续性
p0 = good_new.reshape(-1, 1, 2) # 为下一帧更新特征点
# 在视频中,特征点可以持续跟踪多帧
# 在静态图片对中,只能计算单次运动
网硕互联帮助中心![GUI自动化测试[3]——控件&数鼠标操作-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/01/20260131100458-697dd3ca6f020-220x80.gif)





评论前必须登录!
注册