内容来自OpenCV-Python Tutorials 自己翻译整理
目标:
光流:
光流的概念是指在连续的两帧图像当中,由于图像中的物体移动或者摄像头的移动而使得图像中的目标的运动叫做光流。(说简单点,考虑摄像头不会动的情况,就是一个视频当中有一个运动目标,那么这个视频中的相邻两帧中运动的目标就是光流)
光流是个向量场,表示了一个点从第一帧运动到第二帧的移动。
如图:
上面的图表示了一个球在连续的5帧图像中的运动。箭头表示了它的位移向量。
光流有很多应用场景如下:
等等
光流法的工作原理基于如下假设:
1.连续的两帧图像之间,目标的像素亮度不改变。
2.相邻的像素之间有相似的运动。
考虑第一帧的像素 I(x,y,t) I ( x , y , t ) ,表示在时间t时像素 I(x,y) I ( x , y ) 的值。在经过时间 dt d t 后,此像素在下一帧移动了 (dx,dy) ( d x , d y ) 。
因为这些像素是相同的,而且亮度不变,我们可以表示成, I(x,y,t)=I(x+dx,y+dy,t+dt) I ( x , y , t ) = I ( x + d x , y + d y , t + d t ) 。
假设移动很小,使用泰勒公式可以表示成:
H.O.T是高阶无穷小。
由第一个假设和使用泰勒公式展开的式子可以得到:
改写成
这里设:
∂I∂x=fx ∂ I ∂ x = f x
同理y和t
ΔxΔt=u Δ x Δ t = u
ΔyΔt=v Δ y Δ t = v
fxu+fyv+ft=0 f x u + f y v + f t = 0
上面公式就叫做光流方程,其中 fx f x 和 fy f y 分别是图像的梯度, ft f t 是是图像沿着时间的梯度。但是u和v是未知的,我们没办法用一个方程解两个未知数,那么就有了lucas-kanade这个方法来解决这个问题。
Lucas-Kanade算法:
使用第二条假设,就是所有的相邻像素都有相同的移动。LK算法使用了一个3×3的窗口大小。所以,在这个窗口当中有9个像素点满足公式
fxu+fyv+ft=0 f x u + f y v + f t = 0 。将点代入方程,现在的问题就变成了使用9个点求解两个未知量。
解的个数大于未知数的个数,这是个超定方程,使用最小二乘的方法来求解最优值。如下为计算得到的结果。
(图中的逆矩阵与Harris角点检测很像,说明角点是适合用来做跟踪的)
想法很简单,给出一些点用来追踪,从而获得点的光流向量。但是有另外一个问题需要解决,目前讨论的运动都是小步长的运动,如果有幅度大的运动出现,本算法就会失效。
使用的解决办法是利用图像金字塔。在金字塔顶端的小尺寸图片当中,大幅度的运动就变成了小幅度的运动。所以使用LK算法,可以得到尺度空间上的光流。
OpenCV中的LK光流:
在OpenCV库提供了一个完整的函数,cv2.calcOpticalFlowPyrLK()。
这里,我们可以创建一个简单的应用,用来追踪视频中的一些店。为了探测这些点,我们使用cv2.goodFeaturesToTrack()来实现。
首先选取第一帧,在第一帧图像中检测Shi-Tomasi角点,然后使用LK算法来迭代的跟踪这些特征点。迭代的方式就是不断向cv2.calcOpticalFlowPyrLK()中传入上一帧图片,其中的特征点以及当前帧的图片。函数会返回当前帧的点,这些点带有状态1或者0,如果在当前帧找到了上一帧中的点,那么这个点的状态就是1,否则就是0。
python的OpenCV 光流函数如下
该函数计算基于图像金字塔的稀疏光流
nextPts,status,err = cv.calcOpticalFlowPyrLK( prevImg, nextImg, prevPts, nextPts[, status[, err[, winSize[, maxLevel[, criteria[, flags[, minEigThreshold]]]]]]])
返回值:
输入值:
大致流程就是,首先获取视频或者摄像头的第一帧图像。用goodFeaturesToTrack函数获取初始化的角点,然后开始无限循环获取视频图像帧,将新图像和上一帧图像放入calcOpticalFlowPyrLK函数当中,从而获取新图像的光流。
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
# 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(30) & 0xff
if k == 27:
break
# 更新上一帧的图像和追踪点
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1,1,2)
cv2.destroyAllWindows()
cap.release()
OpenCV中的稠密光流:
LK算法计算的是稀疏的特征点光流,如样例当中计算的是使用 Shi-Tomasi算法得到的特征点。opencv当总提供了查找稠密光流的方法。该方法计算一帧图像当中的所有点。该方法是基于Gunner Farneback提出的一篇论文Two-Frame Motion Estimation Based on Polynomial Expansion。
Farneback稠密光流的主要思想是利用多项式对每个像素的邻域信息进行近似表示,例如考虑二次多项式。
下面样例显示如何找到稠密光流,我们得到的一个两个通道的向量(u,v)。得到的该向量的大小和方向。用不同的颜色编码来使其可视化。
方向与Hue值相关,大小与Value值相关。
使用calcOpticalFlowFarneback函数得到
flow=cv.calcOpticalFlowFarneback(prev, next, flow, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags)
返回值是每个像素点的位移
参数
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
#获取第一帧
ret, frame1 = cap.read()
prvs = cv2.cvtColor(frame1,cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(frame1)
#遍历每一行的第1列
hsv[...,1] = 255
while(1):
ret, frame2 = cap.read()
next = cv2.cvtColor(frame2,cv2.COLOR_BGR2GRAY)
#返回一个两通道的光流向量,实际上是每个点的像素位移值
flow = cv2.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0)
#print(flow.shape)
print(flow)
#笛卡尔坐标转换为极坐标,获得极轴和极角
mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])
hsv[...,0] = ang*180/np.pi/2
hsv[...,2] = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX)
rgb = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)
cv2.imshow('frame2',rgb)
k = cv2.waitKey(30) & 0xff
if k == 27:
break
elif k == ord('s'):
cv2.imwrite('opticalfb.png',frame2)
cv2.imwrite('opticalhsv.png',rgb)
prvs = next
cap.release()
cv2.destroyAllWindows()