OpenCV-Python系列之稀疏光流
之前介绍的两种算法对于视频中的跟踪而言仍然有一定的局限性。这次我们来讨论一种光流估计的方法用于进行目标跟踪。
光流是物体或者摄像头的运动导致的两个连续帧之间的图像对象的视觉运动的模式。它是一个向量场,每个向量是一个位移矢量,显示了从第一帧到第二帧的点的移动,如图:
它显示了一个球在5个连续帧里的移动。箭头显示了它的位移矢量。光流在很多领域有应用:
·移动构建
·视频压缩
·视频稳定
光流在很多假设下有效:
1.物体像素强度在连续帧之间不变化
2.邻居像素有相似运动
考虑第一帧里的一个像素I(x,y,t)(检查新的维度,时间)。它在dt时间后的下一帧移动了(dx, dy)。所以因为那些像素都一样,强度也不变化。我们可以认为:
然后对右边做泰勒级数近似。除以dt得到下面的等式:
其中:
上面的等式被叫做光流等式,我们可以找到fx和fy,他们是图像梯度。类似的ft 是沿时间的梯度。但是(u, v)是未知的。我们无法解出这个等式。所以有一些方法来提供解决这个问题,其中一个是Lucas-Kanade。是由Bruce D. Lucas and Takeo Kanade两位作者提出来的,所以又被称为KLT。
KLT算法工作有三个假设前提条件:
· 亮度恒定
· 短距离移动
· 空间一致性
Opencv中使用cv2.calcOpticalFlowPyrLK()函数计算一个稀疏特征集的光流,使用金字塔中的迭代 Lucas-Kanade 方法。来看函数原型:
nextPts,status,err = cv2.calcOpticalFlowPyrLK(prevImg, #上一帧图片 nextImg, #当前帧图片 prevPts, #上一帧找到的特征点向量 nextPts #与返回值中的nextPtrs相同 [, status[, err[, winSize [, maxLevel[, criteria [, flags[, minEigThreshold]]]]]]])
参数解释:
prevImg--> 上一帧图片;
nextImg--> 当前帧图片;
prevPts--> 上一帧找到的特征点向量;
nextPts--> 与返回值中的nextPtrs相同;
status--> 与返回的status相同;
err--> 与返回的err相同;
winSize--> 在计算局部连续运动的窗口尺寸(在图像金字塔中),default=Size(21, 21);
maxLevel--> 图像金字塔层数,0表示不使用金字塔, default=3;
criteria--> 寻找光流迭代终止的条件;
flags--> 有两个宏,表示两种计算方法,分别是OPTFLOW_USE_INITIAL_FLOW表示使用估计值作为寻找到的初始光流,OPTFLOW_LK_GET_MIN_EIGENVALS表示使用最小特征值作为误差测量,default=0;
minEigThreshold--> 该算法计算光流方程的2×2规范化矩阵的最小特征值,除以窗口中的像素数; 如果此值小于minEigThreshold,则会过滤掉相应的功能并且不会处理该光流,因此它允许删除坏点并获得性能提升, default=1e-4.
返回值:
nextPtrs--> 输出一个二维点的向量,这个向量可以是用来作为光流算法的输入特征点,也是光流算法在当前帧找到特征点的新位置(浮点数);
status--> 标志,在当前帧当中发现的特征点标志status==1,否则为0;
err--> 向量中的每个特征对应的错误率.
实现原理: 在第一帧图像中检测Shi-Tomasi角点,使用LK算法来迭代的跟踪这些特征点。迭代的方式就是不断向cv2.calcOpticalFlowPyrLK()中传入上一帧图片的特征点以及当前帧的图片。函数会返回当前帧的点,这些点带有状态1或者0,如果在当前帧找到了上一帧中的点,那么这个点的状态就是1,否则就是0。
实现流程:
· 加载视频。
· 调用cv2.GoodFeaturesToTrack 函数寻找兴趣点(关键点)。
· 调用cv2.CalcOpticalFlowPyrLK 函数计算出两帧图像中兴趣点的移动情况。
· 删除未移动的兴趣点。
· 在两次移动的点之间绘制一条线段。
我们仍然使用之前的示例视频。
来看代码:
import numpy as np import cv2 cap = cv2.VideoCapture("test.avi") # ShiTomasi 角点检测参数 feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7, blockSize=7) # 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)) # 获取第一帧,找到角点 ret, old_frame = cap.read() # 找到原始灰度图 old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY) # 获取图像中的角点,返回到p0中 p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params) # 创建一个蒙版用来画轨迹 mask = np.zeros_like(old_frame) while (1): 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) # 选取好的跟踪点 good_new = p1[st == 1] good_old = p0[st == 1] # 画出轨迹 for i, (new, old) in enumerate(zip(good_new, good_old)): a, b = new.ravel() c, d = old.ravel() mask = cv2.line(mask, (a, b), (c, d), color[i].tolist(), 2) frame = cv2.circle(frame, (a, b), 5, color[i].tolist(), -1) img = cv2.add(frame, mask) cv2.imshow('frame', img) k = cv2.waitKey(100) & 0xff if k == 27: break # 更新上一帧的图像和追踪点 old_gray = frame_gray.copy() p0 = good_new.reshape(-1, 1, 2) cv2.destroyAllWindows() cap.release()
结果示例: