OpenCV Python Optical Flow (光流)
【目标】
- 了解光流的概念和用 Lucas-Kanade 算法估计光流
- 我们将使用cv2.calcOpticalFlowPyrLK()这样的函数来跟踪视频中的特征点。
- 们将使用cv2.calcOpticalFlowFarneback()方法创建一个密集的光流场,可以用于前景检测。
【理论】
光流是由物体或相机的运动引起的图像物体在两个连续帧之间的表观运动模式。这是一个二维矢量场,其中每个矢量都是一个位移矢量,显示点从第一帧到第二帧的运动。
它显示了一个球在连续5帧中的移动,箭头表示位移矢量。光流有很多应用领域,如:
运动结果
视频压缩
视频稳定。。。
光流的工作原理有以下几个假设:
目标的像素亮度在连续帧中是恒定的。
相邻像素有相似的运动;
假设第一帧中有一个像素 I ( x , y , t ) I(x, y, t) I(x,y,t),这里增加了一个 时间维度 t t t, 下一帧中移动了 ( d x , d y ) (dx, dy) (dx,dy), 因为像素的亮度没有改变,所以
I ( x , y , t ) = I ( x + d x , y + d y , t + d t ) I(x, y, t) = I(x+dx, y+dy, t+dt) I(x,y,t)=I(x+dx,y+dy,t+dt)
对上式中的右边进行泰勒级数展开,去掉公约数并且除以 d t dt dt,得到下式
f x u + f y v + f t = 0 f_xu+f_yv+f_t=0 fxu+fyv+ft=0
其中:
f x = ∂ f ∂ x f y = ∂ f ∂ y u = d x d t , v = d y d t f_x = \frac{\partial f}{\partial x} f_y = \frac{\partial f}{\partial y} \\ u = \frac{dx}{dt}, v = \frac{dy}{dt} fx=∂x∂ffy=∂y∂fu=dtdx,v=dtdy
上面的方程为光流方程, f x , f y f_x,f_y fx,fy为图像的梯度, f t f_t ft为图像在时间上的梯度,但是 ( u , v ) (u,v) (u,v)是不知道的,一个方程不能求解两个未知参数,所以,有很多方法来求解该问题,例如其中的 Lucas-Kanade 方法。
- Lucas-Kanade 方法
之前有一个假设,那就是相邻像素有相似的运动。Lucas-Kanade方法在点周围取 3 ∗ 3 3*3 3∗3块,所以,9个点的运动方向相同,可以求出9个点的 ( f x , f y , f t ) (f_x, f_y,f_t) (fx,fy,ft),可以用最小二乘法得到较好的解。
[ u v ] = [ ∑ f x i 2 ∑ f x i f y i ∑ f x i f y i ∑ f y i 2 ] − 1 [ − ∑ f x i f t i − ∑ f y i f t i ] \begin{bmatrix} u\\v \end{bmatrix} = \begin{bmatrix} \sum f_{x_i}^2 & \sum f_{x_i}f_{y_i}\\ \sum f_{x_i}f_{y_i} & \sum f_{y_i}^2 \end{bmatrix}^{-1} \begin{bmatrix} -\sum f_{x_i}f_{t_i}\\ -\sum f_{y_i}f_{t_i} \end{bmatrix} [uv]=[∑fxi2∑fxifyi∑fxifyi∑fyi2]−1[−∑fxifti−∑fyifti]
用harris角点检测器来检查逆矩阵的相似性,表明角点确实是跟踪的比较好的点。
所以从用户的角度来看,想法很简单,我们给一些点来跟踪,我们得到了这些点的光流矢量,但还是有一些问题,例如大的运动就失效了,为了解决这个问题,我们使用金字塔,当金字塔在上升时,小的运动被移除了,大的运动变成了小的运动,所以通过运用 Lucas-Kanade, 我们得到了光流。
【代码】
import numpy as np
import cv2
videoname = "assets/slow_traffic_small.mp4"
cap = cv2.VideoCapture(videoname)
# 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 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
# Create a mask image for drawing purposes
mask = np.zeros_like(old_frame)
while (1):
ret, frame = cap.read()
if not ret:
print('No frames grabbed!')
break
# 灰度化
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 计算光流
p1, st, err = cv2.calcOpticalFlowPyrLK(
old_gray, frame_gray, p0, None, **lk_params)
# 选择好的跟踪点
if p1 is not None:
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, (int(a), int(b)),
(int(c), int(d)), color[i].tolist(), 2)
frame = cv2.circle(frame, (int(a), int(b)), 5, color[i].tolist(), -1)
img = cv2.add(frame, mask)
cv2.imshow('optical_flow', img)
k = cv2.waitKey(30) & 0xff
if k == 27:
break
# Now update the previous frame and previous points
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1, 1, 2)
cv2.destroyAllWindows()
- Dense Optical Flow(稠密光流)
Lucas-Kanade方法计算稀疏特征集的光流(在我们的例子中,使用Shi-Tomasi算法检测角)。OpenCV提供了另一种查找密集光流的算法。它计算帧中所有点的光流。它基于Gunnar Farneback的算法,Gunnar Farneback在2003年的“基于多项式展开的两帧运动估计”中解释了该算法。
import numpy as np
import cv2
cap = cv2.VideoCapture(cv2.samples.findFile("assets/vtest.avi"))
ret, frame1 = cap.read()
prvs = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(frame1)
hsv[..., 1] = 255
while (1):
ret, frame2 = cap.read()
if not ret:
print('No frames grabbed!')
break
next = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
flow = cv2.calcOpticalFlowFarneback(
prvs, next, None, 0.5, 3, 15, 3, 5, 1.2, 0)
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)
bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
cv2.imshow('DenseOpticalFlow', bgr)
cv2.imshow('src', frame2)
k = cv2.waitKey(30) & 0xff
if k == 27:
break
elif k == ord('s'):
cv2.imwrite('opticalfb.png', frame2)
cv2.imwrite('opticalhsv.png', bgr)
prvs = next
cv2.destroyAllWindows()
【接口】
- calcOpticalFlowPyrLK
cv2.calcOpticalFlowPyrLK( prevImg, nextImg, prevPts, nextPts[, status[, err[, winSize[, maxLevel[, criteria[, flags[, minEigThreshold]]]]]]] ) -> nextPts, status, err
利用Lucas-Kanade方法和金字塔为稀疏特征点计算光流,
- prevImg: 上一张 8位 图像,
- nextImg: 第二张输入图像,尺寸和类型与上一张一样
- prevPts: 2D points 的 vector,需要计算光流的点
- nextPts: 输出的二维点集,在第二张图像中计算出来的特征点的位置;
- status: 输出的状态,如果找到了对应的点,则为1,否则为0
- err: 输出的误差,每一个元素就是对应点的误差,如果没有找到光流,则误差没有定义;
- winSize: 搜索窗口
- maxLevel: 0层为最大的金字塔层数,如果设置为0,则不用金字塔,如果设置为1,则使用两层。。。
- criterial: 终止条件,可以设置最大次数或者最小误差;
- flags: 操作标志: OPTFLOW_USE_INITIAL_FLOW & OPTFLOW_LK_GET_MIN_EIGENVALS
- minEigThreshold: 最小特征值,如果特征值小于这个阈值,则对应的特征会被过滤掉,或者不处理,
- goodFeaturesToTrack
【OpenCV-Python】教程:4-3 Shi-Tomasi 角点检测
- calcOpticalFlowFarneback
cv2.calcOpticalFlowFarneback( prev, next, flow, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags ) -> flow
利用Gunnar Farneback 算法计算稠密光流
- prev: 第一张8位单通道图像
- next: 第二张8位单通道图像
- flow: 输出的光流,类型为 CV_32FC2,一个是方向,一个是梯度
- pyr_scale: 金字塔缩放的长度,如果为0.5,则是经典的金字塔图像,即下一层是前一层的一半;
- levels: 包含初始图像的层级, levels=1则没有创建额外的层,只使用原始的图像
- winsize: 平均窗口尺寸,值越大,会增加算法的鲁棒性可以应对快速的运动
- iterations: 迭代次数
- poly_n: 每个像素的多项式展开,值越大,越平滑,一般设置为5或7
- poly_sigma: 标准差,如果 poly_n 为5, 则为1.1, 如果 poly_n 为 7, 则设置为 1.5
- flags: OPTFLOW_USE_INITIAL_FLOW, OPTFLOW_FARNEBACK_GAUSSIAN
p r e v ( y , x ) ∼ n e x t ( y + f l o w ( y , x ) [ 1 ] , x + f l o w ( y , x ) [ 0 ] ) prev(y,x)∼next(y+flow(y,x)[1],x+flow(y,x)[0]) prev(y,x)∼next(y+flow(y,x)[1],x+flow(y,x)[0])
【参考】
- OpenCV: Optical Flow
- Object Tracking
- Jean-Yves Bouguet. Pyramidal implementation of the affine lucas kanade feature tracker description of the algorithm. Intel Corporation , 5, 2001.
- Gunnar Farnebäck. Two-frame motion estimation based on polynomial expansion. In Image Analysis , pages 363–370. Springer, 2003.