光流(Optical Flow)原理及其算法示例
光流的概念最早是由Gibson在1950年提出的。它是空间移动物体在像素观察平面中移动的瞬时速度。是一种计算物体在相邻帧间运动信息的方法。
一般来说,光流(Optical Flow)是物体在三维空间中的运动在二维像平面上的投影。它是由物体和相机的相对速度产生的,反映了物体在极小时间内对应的图像像素的运动方向和速度。
Lucas–Kanade方法(KLT)是一种基于光流原理的特征点跟踪算法。本文首先介绍光流的原理,然后介绍了KLT及其相关的KLT变体算法。
光流约束方程
假设I(x,y,t)为时刻t像素点(x,y)的像素值(亮度),该像素点在两个图像帧之间移动了Δx,Δy,Δt。因此我们可以得出相同亮度的结论:
假设运动很小,我们可以从泰勒级数推导一阶泰勒展开式:
因此,
其中(dx/dt, dy/dt) = (u, v)为待解像素的光流。
(∂I/∂x,∂I/∂y) = (I_x, I_y)是像素灰度空间微分,t = I_x是像素坐标点的时间灰度微分。
整理成矩阵形式:
该公式表明,同一坐标位置上的时间灰度微分是空间灰度微分与该位置上相对于观测者的速度的乘积。假设空间一致性,对于周围的多个点,有:
这个方程组的方程多于未知数,因此通常是超定的。Lucas-Kanade方法通过最小二乘原理获得折衷解决方案:
这就是光流算法的孔径问题。为了找到光流,需要另一组方程,并附加约束条件。所有的光流方法都引入了估算实际流的附加条件。
局部差分法:Lucas-Kanade算法
为了使方程可求解,进行以下假设:
- 亮度是恒定的,图像中对象的像素亮度在连续帧之间不会改变;
- 短距离(短期)运动,相邻帧之间的时间足够短,并且物体运动很小;
- 空间一致性,相邻像素具有相似的运动;
恒定亮度是指某些像素的跟踪不随时间变化:
公式表示被跟踪像素的灰度不随时间变化:
连续时间意味着相邻帧之间的运动很小。换句话说,运动的变化可以被认为是亮度相对于时间的导数。一维空间的示例:
其中,I_x是图像的偏导数,I_t是图像随时间的导数,v是所需速度。
与之前直接比较像素灰度值的方法不同,Lucas-Kanade算法比较像素周围的窗口像素以找到最相似的像素。假设光流在短时间τ内,前后两帧满足:
其中D为变形矩阵,d称为位移向量,D表示两像素窗口块移动后的变形量,因此,当窗口较小时,将更加难以估计。通常,D可用于测量两个像素窗口的相似度,即特征点是否漂移。对于光流跟踪,通常仅考虑平移模型:
为了通用性,我们使用仿射运动模型来推导Lucas-Kanade算法的原理。在像素窗口下,构造误差函数:
其中w(x)是权重函数,可以定义为高斯函数。分别对变量d和D进行微分:
其中
记住光流u = Dx + d,然后对运动后的像素点进行泰勒展开:
这里给出了平移运动模型的结果。设D = 0:
其中Z是2×2矩阵,e是2×1向量。这是一个线性方程优化问题。当Z是可逆的时,可以轻松地求解该方程。由于泰勒展开式是在推导过程中使用的,因此仅当像素位移较小时才成立。在实际操作中,通常每次将最后结果用于初始化和进一步解决时(在Newton-Raphson Fasion中),都需要迭代解决。
工作实例
在光流可以工作之前,我们必须给它提供一组关键点以在两个图像帧之间进行跟踪。
在下面的示例中,我们使用了Shi-Tomasi corner detector,该detector使用与Harris corner detector相同的过程来查找构成图像中“角点”的强度模式,只是它增加了一个额外的参数来帮助选择最突出的角点。
# parameters for ShiTomasi corner detectionfeature_params = dict( maxCorners = 10, qualityLevel = 0.2, minDistance = 5, blockSize = 5 )# convert all frames to grayscalegray_1 = cv2.cvtColor(frame_1, cv2.COLOR_RGB2GRAY)gray_2 = cv2.cvtColor(frame_2, cv2.COLOR_RGB2GRAY)gray_3 = cv2.cvtColor(frame_3, cv2.COLOR_RGB2GRAY)# Take first frame and find corner points in itpts_1 = cv2.goodFeaturesToTrack(gray_1, mask = None, **feature_params)# display the detected pointsplt.imshow(frame_1)for p in pts_1: # plot x and y detected points plt.plot(p[0][0], p[0][1], 'r.', markersize=15)# print out the x-y locations of the detected pointsprint(pts_1)
一旦我们在我们感兴趣的初始图像上检测到关键点,我们就可以计算这幅图像(帧1)和下一帧(帧2)之间的光流。它接收初始图像帧、下一幅图像帧和一组点,并返回下一帧中检测到的点和一个值,该值指示从一帧到下一帧的点之间的匹配程度。
这些参数还包括窗口大小和maxLevels,它们指示窗口的大小以及将使用pyramid缩放来缩放给定图像的级数。对匹配点进行迭代搜索,最后一个参数反映了这个匹配条件(如果使用不同的图像,您可能需要更改这些值,但是这些值应该适用于提供的示例)。Python示例代码如下
# parameters for lucas kanade optical flowlk_params = dict( winSize = (5,5), maxLevel = 2, criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))# calculate optical flow between first and second framepts_2, match, err = cv2.calcOpticalFlowPyrLK(gray_1, gray_2, pts_1, None, **lk_params)# Select good matching points between the two image framesgood_new = pts_2[match==1]good_old = pts_1[match==1]
接下来,让我们显示最终的运动向量,Python示例代码如下
# create a mask image for drawing (u,v) vectors on top of the second framemask = np.zeros_like(frame_2)# draw the lines between the matching points (these lines indicate motion vectors)for i,(new,old) in enumerate(zip(good_new,good_old)): a,b = new.ravel() c,d = old.ravel() # draw points on the mask image mask = cv2.circle(mask,(a,b),5,(200),-1) # draw motion vector as lines on the mask image mask = cv2.line(mask, (a,b),(c,d), (200), 3) # add the line image and second frame togethercomposite_im = np.copy(frame_2)composite_im[mask!=0] = [0]plt.imshow(composite_im)