光流法是空间运动物体在观察成像平面上的像素运动的瞬时速度,是利用图像序列中像素在时间域上的变化以及相邻帧之间的相关性来找到上一帧跟当前帧之间存在的对应关系,从而计算出相邻帧之间物体的运动信息的一种方法。一般而言,光流是由于场景中前景目标本身的移动、相机的运动,或者两者的共同运动所产生的。
简单来说,光流是空间运动物体在观测成像平面上的像素运动的“瞬时速度”。光流的研究是利用图像序列中的像素强度数据的时域变化和相关性来确定各自像素位置的“运动”。研究光流场的目的就是为了从图片序列中近似得到不能直接得到的运动场。
光流法的前提假设:
(1)相邻帧之间的颜色恒定,对于灰度图来说,亮度恒定;
(2)相邻视频帧的取帧时间连续,或者,相邻帧之间物体的运动比较“微小”;
(3)保持空间一致性;即,同一子图像的像素点具有相同的运动
上面的三个假设对光流法的原理公式推导很重要,具体的推导公式不在具体阐述。同时也是判断使用光流法的使用条件,光流法主要分为两种,计算部分像素运动的:稀疏光流,以Lucas-Kanade为代表,计算所有像素运动的:稠密光流。下面将结合代码具体理解。
L-K算法基于上面三个假设。
(1)亮度恒定,就是同一点随着时间的变化,其亮度不会发生改变。这是基本光流法的假定(所有光流法变种都必须满足),用于得到光流法基本方程;
(2)小运动,这个也必须满足,就是时间的变化不会引起位置的剧烈变化,这样灰度才能对位置求偏导(换句话说,小运动情况下我们才能用前后帧之间单位位置变化引起的灰度变化去近似灰度对位置的偏导数),这也是光流法不可或缺的假定;
(3)空间一致,一个场景上邻近的点投影到图像上也是邻近点,且邻近点速度一致。这是Lucas-Kanade光流法特有的假定,因为光流法基本方程约束只有一个,而要求x,y方向的速度,有两个未知变量。我们假定特征点邻域内做相似运动,就可以连立区域内个个像素点的多个方程求取x,y方向的速度。
这样假设的具体原因可参考:https://blog.csdn.net/zyazky/article/details/52175069
L-K光流法是稀疏光流法,需要找到视频每帧的特征点,这个常用的有Shi-Tomasi角点和SIFT特征点。
opencv中调用goodFeaturesToTrack函数来实现角点检测
void goodFeaturesToTrack( InputArray image, OutputArray corners,
int maxCorners, double qualityLevel, double minDistance,
InputArray mask=noArray(), int blockSize=3,
bool useHarrisDetector=false, double k=0.04 );
第一个参数image:8位或32位单通道灰度图像;
第二个参数corners:位置点向量,保存的是检测到的角点的坐标;
第三个参数maxCorners:定义可以检测到的角点的数量的最大值;
第四个参数qualityLevel:检测到的角点的质量等级,角点特征值小于qualityLevel*最大特征值的点将被舍弃;
第五个参数minDistance:两个角点间最小间距,以像素为单位;
第六个参数mask:指定检测区域,若检测整幅图像,mask置为空Mat();
第七个参数blockSize:计算协方差矩阵时窗口大小;默认为3
第八个参数useHarrisDetector:是否使用Harris角点检测,为false,则使用Shi-Tomasi算子;
第九个参数k:留给Harris角点检测算子用的中间参数,一般取经验值0.04~0.06。第八个参数为false时,该参数不起作用;
mask可以指定角点产生的区域,划定检测范围
opencv中提供了一个函数来实现K-L算法:cv2.calcOpticalFlowPyrLK()
nextPts,status,err = cv.calcOpticalFlowPyrLK( prevImg, nextImg, prevPts, nextPts[, status[, err[, winSize[, maxLevel[, criteria[, flags[, minEigThreshold]]]]]]])
返回值:
nextPtrs:输出一个二维点的向量,这个向量可以是用来作为光流算法的输入特征点,也是光流算法在当前帧找到特征点的新位置(浮点数)
status 标志:在当前帧当中发现的特征点标志status==1,否则为0
err :向量中的每个特征对应的错误率
输入值:
prevImg :上一帧图片
nextImg :当前帧图片
prevPts :上一帧找到的特征点向量
nextPts :与返回值中的nextPtrs相同
status :与返回的status相同
err :与返回的err相同
winSize :在计算局部连续运动的窗口尺寸(在图像金字塔中)
maxLevel: 图像金字塔层数,0表示不使用金字塔
criteria :寻找光流迭代终止的条件
flags :有两个宏,表示两种计算方法,分别是OPTFLOW_USE_INITIAL_FLOW:表示使用估计值作为寻找到的初始光流,OPTFLOW_LK_GET_MIN_EIGENVALS表示:使用最小特征值作为误差测量
minEigThreshold :该算法计算光流方程的2×2规范化矩阵的最小特征值,除以窗口中的像素数; 如果此值小于minEigThreshold,则会过滤掉相应的功能并且不会处理该光流,因此它允许删除坏点并获得性能提升。
下面附上代码:
读取视频第一帧,检测角点,然后使用K-L算法来迭代跟踪每个角点在每一帧的位置信息,最后画出运动轨迹。在做煤矿安全生产的项目时,考虑过使用光流法来检测传送到运动方向和速度,但是视频灰度图像中,运煤的传送带的灰度图亮度差异太小,导致角点产生了跳变。
import numpy as np
import cv2
import matplotlib.pyplot as plt
cap = cv2.VideoCapture('1/12311sc.mp4')
# params for ShiTomasi corner detection 特征点检测
feature_params = dict( maxCorners = 10,
qualityLevel = 0.1,
minDistance = 10,
blockSize = 3 )
# Parameters for lucas kanade optical flow光流法参数
lk_params = dict(winSize = (15,15),
maxLevel = 0,
criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
# Create some random colors 画轨迹
color = np.random.randint(0,255,(100,3))
# Take first frame and find corners in it
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
roi = np.zeros_like(old_gray)
x,y,w,h = 266,143,150,150
roi[y:y+h, x:x+w] = 255
p0 = cv2.goodFeaturesToTrack(old_gray, mask = roi, **feature_params)
# Create a mask image for drawing purposes
mask = np.zeros_like(old_frame)
while(1):
ret,frame = cap.read()
if not ret:
break
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# calculate optical flow
p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
# Select good points
good_new = p1[st==1]
good_old = p0[st==1]
# draw the tracks
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),3,color[i].tolist(),-1)
img = cv2.add(frame,mask)
cv2.imshow('frame',img)
key = cv2.waitKey(60) & 0xff
if key == 27: # 按下ESC时,退出
break
elif key == ord(' '): # 按下空格键时,暂停
cv2.waitKey(0)
# Now update the previous frame and previous points
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1,1,2)
cv2.destroyAllWindows()
cap.release()
如有疑问或错误,欢迎大家指正交流。