目录
一、前置知识:什么是光流 & 金字塔 LK 算法
1. 光流基础概念
2. Lucas-Kanade(LK)算法约束条件
3. 整体实现流程
二、完整可运行源码(带原注释 + 补充修复)
三、分模块逐行深度解析
模块 1:视频初始化与第一帧预处理
模块 2:Shi-Tomasi 角点检测参数 & goodFeaturesToTrack详解
字典参数解释
函数返回值p0
模块 3:轨迹画布 mask 初始化
模块 4:金字塔 LK 光流核心参数
模块 5:主循环读取帧 & 灰度转换
模块 6:核心光流函数 calcOpticalFlowPyrLK 逐参数拆解
输入参数说明
三个返回值(跟踪核心)
模块 7:筛选有效跟踪点 & 绘制运动轨迹
模块 8:轨迹叠加与画面展示
模块 9:更新跟踪参考点,持续循环
模块 10:资源释放收尾
四、运行前置依赖与踩坑解决
1. 安装依赖库
2. 常见报错 & 解决方案
报错 1:视频读取为空,画面全黑
报错 2:calcOpticalFlowPyrLK坐标维度不匹配
报错 3:轨迹不显示,mask 全黑
报错 4:运动速度过快,特征点频繁丢失
五、拓展优化方向
六、总结
一、前置知识:什么是光流 & 金字塔 LK 算法
1. 光流基础概念
光流是视频中相邻两帧像素的瞬时运动矢量,通过像素灰度变化推导物体移动方向与距离,分为两类:
- 稠密光流:计算图像每一个像素的运动矢量,计算量大;
- 稀疏光流:只跟踪少量高质量特征角点,速度快、工业场景常用,本文采用该方案。
前提:
(1)亮度恒定:同一点随着时间的变化,其亮度不会发生改变。
(2)小运动:随着时间的变化不会引起位置的剧烈变化,只有小运动情况下才能用前后帧之间单位位置变化引起的灰度变化去近似灰度对位置的偏导数。
(3)空间一致:一个场景上邻近的点投影到图像上也是邻近点,且邻近点速度一致。因为光流法基本方程约束只有一个,而要求x,y方向的速度,有两个未知变量。所以需要连立n多个方程求解。
2. Lucas-Kanade(LK)算法约束条件
LK 算法成立依赖 3 个假设:
- 亮度恒定:同一特征点前后帧灰度不变;
- 小运动:两帧之间物体位移很小(基础 LK 缺陷,快速运动容易跟丢);
- 空间一致性:相邻像素运动方向一致。
3. 整体实现流程
- 读取视频第一帧,灰度化;
- Shi-Tomasi 检测高质量角点作为初始跟踪特征;
- 创建空白 mask 画布,用于永久绘制运动轨迹;
- 循环读取视频每一帧,灰度化;
calcOpticalFlowPyrLK计算前后帧角点光流;- 筛选跟踪成功的特征点,在 mask 上绘制轨迹线段;
- mask 轨迹叠加原图可视化,ESC 键退出循环;
- 更新前一帧图像与特征点,持续跟踪。
二、完整可运行源码(带原注释 + 补充修复)
import numpy as np import cv2 # ----------------------1. 初始化视频读取---------------------- # 打开视频文件,传入0则读取摄像头实时画面 cap = cv2.VideoCapture('test.avi') # 随机生成100组RGB颜色,每个特征点分配独立颜色,区分不同运动轨迹 color = np.random.randint(0, 255, (100, 3)) # 读取视频第一帧,作为光流计算的初始参考帧 ret, old_frame = cap.read() # 将彩色BGR帧转为灰度图:光流、角点检测仅支持单通道灰度图 old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY) # ----------------------2. Shi-Tomasi角点检测参数配置---------------------- feature_params = dict( maxCorners=100, # 最大角点数量,限制跟踪点总数,避免画面杂乱 qualityLevel=0.3, # 角点质量阈值:最大角点响应值 * 0.3,低于该值直接丢弃 minDistance=7 # 两个角点之间最小欧式距离,避免特征点扎堆 ) # goodFeaturesToTrack:Shi-Tomasi角点检测函数,提取第一帧跟踪特征点 # 参数:灰度图、掩码(None全图检测)、字典解包传入检测参数 p0 = cv2.goodFeaturesToTrack(old_gray, mask=None,** feature_params) # 创建和原图尺寸完全一致的全黑空白掩码,专门用来绘制永久运动轨迹 mask = np.zeros_like(old_frame) # ----------------------3. 金字塔LK光流计算参数配置---------------------- lk_params = dict( winSize=(15, 15), # 光流搜索窗口大小,窗口越大抗噪越强,速度越慢 maxLevel=2 # 金字塔层数,0=不使用金字塔,数值越大处理大运动能力越强 ) # ----------------------4. 视频帧循环跟踪主逻辑---------------------- while True: # 读取下一帧画面 ret, frame = cap.read() # ret=False代表视频读取完毕,跳出循环 if not ret: break # 当前帧转为灰度单通道图,满足光流计算输入要求 frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # ----------------------核心函数:金字塔LK光流计算---------------------- # calcOpticalFlowPyrLK(前帧灰度图, 当前帧灰度图, 前帧特征点, 输出点容器, 光流参数) # 返回值:p1当前帧特征点坐标、st跟踪状态、err匹配误差 p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, nextPts=None, **lk_params) # 筛选跟踪成功的特征点:st==1代表该点成功匹配,0代表丢失/遮挡 good_new = p1[st == 1] good_old = p0[st == 1] # 遍历每一组成功跟踪的新旧特征点,绘制运动轨迹 for i, (new, old) in enumerate(zip(good_new, good_old)): a, b = new # 当前帧特征点坐标 (x,y) c, d = old # 上一帧特征点坐标 (x,y) # OpenCV绘图函数坐标必须是整数,浮点坐标强制转换int a, b, c, d = int(a), int(b), int(c), int(d) # 在黑色mask画布绘制线段,连接前后帧同一点,永久保留轨迹 mask = cv2.line(mask, pt1=(a, b), pt2=(c, d), color=color[i].tolist(), thickness=2) # 单独展示轨迹画布(可选,调试用) cv2.imshow('mask', mask) # 将轨迹mask与原始彩色帧叠加,实现原图+彩色轨迹可视化 img = cv2.add(frame, mask) # 展示最终跟踪画面 cv2.imshow('frame', img) # 等待150ms,控制视频播放速度;检测ESC按键(键值27)退出 k = cv2.waitKey(150) if k == 27: break # 更新循环变量:当前帧变为下一轮的前帧,当前特征点变为上一轮点 old_gray = frame_gray.copy() # reshape重构数组维度:(N,2) → (N,1,2),适配calcOpticalFlowPyrLK输入格式 p0 = good_new.reshape(-1, 1, 2) # 循环结束,释放视频资源、销毁所有窗口 cap.release() cv2.destroyAllWindows()这里在下面这行代码里的cv2.VideoCapture('test.avi')函数内放入需要进行光流估计的视频即可,视频名字改为test.avi之后放入当前文件夹内
cap = cv2.VideoCapture('test.avi')下面是运行效果:
三、分模块逐行深度解析
模块 1:视频初始化与第一帧预处理
cap = cv2.VideoCapture('test.avi') color = np.random.randint(0, 255, (100, 3)) ret, old_frame = cap.read() old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)cv2.VideoCapture('test.avi'):读取本地视频;参数填0可调用电脑摄像头实时跟踪;np.random.randint(0,255,(100,3)):生成 100 组随机 RGB 三通道颜色,每个特征点一条专属彩色轨迹,区分不同物体运动;cap.read():读取一帧视频画面,返回ret(读取状态布尔值)、old_frame(BGR彩色图像);cv2.COLOR_BGR2GRAY:彩色转灰度,角点检测、光流算法仅支持单通道灰度图,三通道彩色无法直接计算。
模块 2:Shi-Tomasi 角点检测参数 &goodFeaturesToTrack详解
feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7) p0 = cv2.goodFeaturesToTrack(old_gray, mask=None,** feature_params)字典参数解释
| 参数 | 作用 | 调参技巧 |
|---|---|---|
| maxCorners=100 | 最多检测 100 个角点 | 画面运动物体多则调大,画面简单调小,减少计算量 |
| qualityLevel=0.3 | 角点质量筛选系数 | 数值越大筛选越严格,角点越少;0.3 为通用平衡值 |
| minDistance=7 | 角点最小间隔像素 | 防止大量角点聚集在同一小块区域,保证特征均匀分布 |
函数返回值p0
输出数组形状(N,1,2),N 是检测到的角点数量,每个元素存储[x,y]像素坐标,是光流跟踪的初始输入点。
模块 3:轨迹画布 mask 初始化
mask = np.zeros_like(old_frame)np.zeros_like(old_frame):创建和原图宽高、通道数完全一致的全黑数组;- 核心作用:单独存储所有运动轨迹线段,不会随帧刷新消失;每一帧只新增线段,历史轨迹永久保留;最后通过
cv2.add叠加到原图实现可视化。
模块 4:金字塔 LK 光流核心参数
lk_params = dict(winSize=(15, 15), maxLevel=2)这里注意:
maxLevel 数值怎么选
maxLevel=0:不使用金字塔,只用原图,只能跟踪微小移动;
maxLevel=1~3:日常视频、摄像头跟踪最常用;
数值越大,能跟踪更大幅度的运动,但计算速度变慢、消耗更多内存。
| 参数 | 含义 | 调参场景 |
|---|---|---|
| winSize=(15,15) | 局部搜索窗口尺寸 | 物体运动模糊、噪声大时调大 (21,21);小物体精细跟踪调小 (7,7) |
| maxLevel=2 | 金字塔层数 | 0 = 关闭金字塔,仅微小运动可用;2~3 适配大部分视频快速运动场景;层数越高速度越慢 |
模块 5:主循环读取帧 & 灰度转换
ret, frame = cap.read() if not ret: break frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)if not ret:视频文件读取到末尾时ret=False,直接退出循环,避免空图报错;- 每帧必须转灰度,前后帧灰度图是
calcOpticalFlowPyrLK强制输入要求。
模块 6:核心光流函数calcOpticalFlowPyrLK逐参数拆解
p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, nextPts=None, **lk_params)输入参数说明
old_gray:上一帧灰度图像(金字塔 LK 前帧);frame_gray:当前帧灰度图像(金字塔 LK 后帧);p0:上一帧检测到的特征角点数组,形状(N,1,2);nextPts=None:输出容器,填 None 时函数自动创建数组存储当前帧匹配点;**lk_params:解包传入金字塔、搜索窗口等光流配置。
三个返回值(跟踪核心)
p1:当前帧匹配成功的特征点坐标,格式同p0;st:状态数组,长度等于特征点数量;st[i]=1表示第 i 个点跟踪匹配成功,0表示点丢失、遮挡、超出画面;err:每个特征点灰度匹配误差,误差越大匹配可信度越低,可用于二次过滤噪声点。
模块 7:筛选有效跟踪点 & 绘制运动轨迹
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)p1[st == 1]:布尔索引筛选,只保留跟踪成功的点,剔除丢失失效角点;zip(good_new, good_old):一一配对当前帧、上一帧同一特征点;int(a),int(b):光流输出坐标是浮点小数,OpenCV 绘图 API 只接收整数像素坐标,必须强制转换;cv2.line(mask,...):在黑色 mask 画布绘制线段,连接前后帧同一特征点,实现轨迹留存;不同点使用独立随机颜色,区分运动目标。
模块 8:轨迹叠加与画面展示
img = cv2.add(frame, mask) cv2.imshow('frame', img) k = cv2.waitKey(150) if k == 27: breakcv2.add(frame, mask):像素级加法融合原图 + 轨迹 mask;黑色 mask 像素值为 0,仅绘制过线段的彩色像素会叠加在原图上;cv2.waitKey(150):帧间隔 150ms,控制视频播放速度,数值越大播放越慢;返回键盘按键 ASCII 码;k == 27:ESC 键 ASCII 码为 27,按下直接跳出跟踪循环。
模块 9:更新跟踪参考点,持续循环
old_gray = frame_gray.copy() p0 = good_new.reshape(-1, 1, 2)old_gray.copy():将当前帧灰度图复制,作为下一轮循环的 “前一帧参考图”;reshape(-1,1,2):核心原因是calcOpticalFlowPyrLK 函数要求输入的特征点坐标是 (N, 1, 2) 格式,而经过筛选后的 good_new 是 (N, 2) 格式。
模块 10:资源释放收尾
cap.release() cv2.destroyAllWindows()cap.release():释放视频读取流,释放内存资源;cv2.destroyAllWindows():关闭所有cv2.imshow创建的窗口,防止程序后台残留窗口进程。
四、运行前置依赖与踩坑解决
1. 安装依赖库
pip install opencv-python opencv-contrib-python numpy2. 常见报错 & 解决方案
报错 1:视频读取为空,画面全黑
- 原因 1:
test.avi视频文件不存在、路径错误; - 解决:把视频和代码放在同一文件夹,或填写完整绝对路径;想用摄像头直接替换
cap = cv2.VideoCapture(0)。
报错 2:calcOpticalFlowPyrLK坐标维度不匹配
- 原因:未执行
reshape(-1,1,2),特征点数组维度为(N,2); - 解决:每次循环末尾必须对
good_new重构三维维度。
报错 3:轨迹不显示,mask 全黑
- 原因 1:
st==1筛选后good_new为空,所有特征点全部丢失; - 解决:调小
qualityLevel、增大maxCorners,增加初始角点数量;降低maxLevel适配微小运动场景。 - 原因 2:视频画面几乎无纹理(纯白墙壁),
goodFeaturesToTrack检测不到任何角点; - 解决:更换纹理丰富的视频素材。
报错 4:运动速度过快,特征点频繁丢失
- 解决:调高
maxLevel=3,扩大金字塔层数;调大winSize=(21,21)搜索窗口。
五、拓展优化方向
- 周期性重新检测角点:长时间跟踪后特征点会大量丢失,每 30 帧重新调用
goodFeaturesToTrack补充新角点; - 绘制特征点圆点:在每一帧画面用
cv2.circle标记当前跟踪角点,直观看到点位置; - 基于运动矢量过滤静止点:计算
good_new - good_old位移,剔除静止背景点,只跟踪移动物体; - 保存跟踪视频:使用
cv2.VideoWriter将带轨迹的画面保存为新视频文件; - 误差过滤:结合
err误差数组,剔除匹配误差过大的噪声跟踪点,提升稳定性。
六、总结
本文完整实现工业界轻量化稀疏光流跟踪方案 —— 金字塔 Lucas-Kanade 算法,相比稠密光流具备计算速度快、资源占用低优势,适合视频目标轨迹追踪、SLAM 前端特征跟踪、摄像头运动分析等场景。核心分为两步:Shi-Tomasi 高质量角点提取 + 金字塔分层光流迭代匹配,搭配独立 mask 画布实现永久运动轨迹可视化,代码可直接落地修改用于摄像头实时跟踪、视频分析项目。
后续会继续更新进阶内容,欢迎关注、点赞、收藏,持续学习~