光流
本小节我们主要理解光流的概念以及使用Lucas-Kanade方法对其进行评估。另外还有如下两个函数: cv.calcOpticalFlowPyrLK() 用于跟踪视频中的特征点, cv.calcOpticalFlowFarneback() 用于创建一个密集光流区域。
- 用法如下
- cv.calcOpticalFlowPyrLK( prevImg, nextImg, prevPts, nextPts[, status[, err[, winSize[, maxLevel[, criteria[, flags[, minEigThreshold]]]]]]] ) -> nextPts, status, err
- cv.calcOpticalFlowFarneback( prev, next, flow, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags ) -> flow
一、光流
光流是由物体或相机的运动引起的两个连续帧之间图像物体的视运动模式。 它是 2D 矢量场,其中每个矢量都是位移矢量,显示点从第一帧到第二帧的移动。 考虑下图(图片提供:维基百科关于光流的文章) 。
上面展示了一个球在5个连续帧中的运动图像,箭头显示其位移矢量。 光流在以下领域有很多应用:
- 运动结构
- 压缩视频
- 视频稳定......
光流需要满足提下条件才会生效:
- 物体的像素灰度强度在连续帧之间不会改变
- 相邻像素之间有着相似的运动(方向步长)
考虑第一帧中的像素 I(x,y,t)(添加一个新维度——时间t。之前我们只处理图像,所以不需要时间)。 它在 dt 时间之后拍摄的下一帧中移动距离 (dx,dy)。 所以由于这些像素是相同的并且强度没有改变,我们可以说,
然后取右边的泰勒级数逼近,去掉常用项,除以dt,得到如下等式:
其中: ,上面的等式被称为光流等式,我们可以看出等式中的 和 就是图像梯度。类似地, 也正是 对 的梯度。但是 值我们并不明确,我们不能用两个未知变量求解这个方程。 因此提供了几种方法来解决这个问题,其中之一是 Lucas-Kanade。
1. Lucas-Kanade 方法
我们之前已经看到一个假设,即所有相邻像素都会有相似的运动。 Lucas-Kanade 方法在该点周围采用 3x3 区域。 所以所有 9 个点都有相同的运动。 我们可以找到这 9 个点的 。 所以现在我们的问题变成了求解 9 个方程,其中两个未知变量是超定的。 用最小二乘拟合法得到更好的解。 下面是最终的解决方案,即两个方程 - 两个未知问题并解决以获得解决方案。
(用Harris角点检测器检查逆矩阵的相似性。它表示角点是更好的跟踪点。)所以从用户的角度来看,这个想法很简单,我们给一些点跟踪,我们接收到的光流向量 那些点。 但是又出现了一些问题。 到目前为止,我们处理的是小移动,所以当有大变动时它会失败。 为了解决这个问题,我们使用金字塔。 当我们在金字塔中上升时,小动作被移除,大动作变成小动作。 因此,通过在那里应用 Lucas-Kanade,我们得到了光流和尺度。
二、OpenCV中的Lucas-Kanade 光流
OpenCV 在一个函数 cv.calcOpticalFlowPyrLK() 中提供了所有这些功能。 在这里,我们创建了一个简单的应用程序来跟踪视频中的一些点。 为了决定点,我们使用 cv.goodFeaturesToTrack()。 我们取第一帧,检测其中的一些 Shi-Tomasi 角点,然后我们使用 Lucas-Kanade 光流迭代跟踪这些点。 对于函数 cv.calcOpticalFlowPyrLK(),我们传递前一帧、前一个点和下一帧。 它返回下一个点以及一些状态编号,如果找到下一个点,则值为 1,否则为零。 我们迭代地将这些下一点作为下一步中的前一点传递。 请看下面的代码:
import numpy as np
import cv2 as cv
import argparse
parser = argparse.ArgumentParser(description='This sample demonstrates Lucas-Kanade Optical Flow calculation. \
The example file can be downloaded from: \
https://www.bogotobogo.com/python/OpenCV_Python/images/mean_shift_tracking/slow_traffic_small.mp4')
parser.add_argument('image', type=str, help='path to image file')
args = parser.parse_args(args = ['slow_traffic_small.mp4'])
cap = cv.VideoCapture(args.image)
# ShiTomasi角点检测的参数
feature_params = dict( maxCorners = 100,
qualityLevel = 0.3,
minDistance = 7,
blockSize = 7 )
# lucas kanade光流的参数
lk_params = dict( winSize = (15, 15),
maxLevel = 2,
criteria = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03))
# 创建随机颜色,最后一个参数是narray阵列尺寸,生成100中BGR颜色
color = np.random.randint(0, 255, (100, 3))
# 获取第一帧并检测其中的角点
ret, old_frame = cap.read()
old_gray = cv.cvtColor(old_frame, cv.COLOR_BGR2GRAY)
p0 = cv.goodFeaturesToTrack(old_gray, mask = None, **feature_params)
# 创建一个便于绘图的掩膜图像
mask = np.zeros_like(old_frame)
while(1):
ret, frame = cap.read()
if not ret:
print('No frames grabbed!')
break
frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
# 传入前一灰度帧,当前灰度帧,前帧需要跟踪的特征点,【当前帧需要跟踪的特征点】,lucas kanade光流的参数
# 得到当前帧需要跟踪的特征点
p1, st, err = cv.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
# 挑选出良好的特征点进行绘制,p1、st都是与原帧大小相等的阵列
if p1 is not None:
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 = cv.line(mask, (int(a), int(b)), (int(c), int(d)), color[i].tolist(), 2)
frame = cv.circle(frame, (int(a), int(b)), 5, color[i].tolist(), -1)
# 将绘出结果进行展示
img = cv.add(frame, mask)
cv.imshow('frame', img)
k = cv.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)
cap.release()
cv.destroyAllWindows()
复制代码
No frames grabbed!
复制代码
(此代码不检查下一个关键点的正确程度。因此,即使图像中的任何特征点消失,光流也有可能找到下一个可能看起来接近它的点。所以实际上对于稳健的跟踪,corner 点应该在特定的时间间隔内检测。OpenCV 样本提供了这样一个样本,它每 5 帧找到特征点。它还对光流点进行反向检查,只选择好的点。代码:samples/python/ lk_track.py)。
#!/usr/bin/env python
'''
Lucas-Kanade tracker
====================
Lucas-Kanade sparse optical flow demo. Uses goodFeaturesToTrack
for track initialization and back-tracking for match verification
between frames.
Usage
-----
lk_track.py [<video_source>]
Keys
----
ESC - exit
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
import video
from common import anorm2, draw_str
lk_params = dict( winSize = (15, 15),
maxLevel = 2,
criteria = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03))
feature_params = dict( maxCorners = 500,
qualityLevel = 0.3,
minDistance = 7,
blockSize = 7 )
class App:
def __init__(self, video_src):
self.track_len = 10
self.detect_interval = 5
self.tracks = []
self.cam = video.create_capture(video_src)
self.frame_idx = 0
def run(self):
while True:
_ret, frame = self.cam.read()
frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
vis = frame.copy()
if len(self.tracks) > 0:
img0, img1 = self.prev_gray, frame_gray
p0 = np.float32([tr[-1] for tr in self.tracks]).reshape(-1, 1, 2)
p1, _st, _err = cv.calcOpticalFlowPyrLK(img0, img1, p0, None, **lk_params)
p0r, _st, _err = cv.calcOpticalFlowPyrLK(img1, img0, p1, None, **lk_params)
d = abs(p0-p0r).reshape(-1, 2).max(-1)
good = d < 1
new_tracks = []
for tr, (x, y), good_flag in zip(self.tracks, p1.reshape(-1, 2), good):
if not good_flag:
continue
tr.append((x, y))
if len(tr) > self.track_len:
del tr[0]
new_tracks.append(tr)
cv.circle(vis, (int(x), int(y)), 2, (0, 255, 0), -1)
self.tracks = new_tracks
cv.polylines(vis, [np.int32(tr) for tr in self.tracks], False, (0, 255, 0))
draw_str(vis, (20, 20), 'track count: %d' % len(self.tracks))
if self.frame_idx % self.detect_interval == 0:
mask = np.zeros_like(frame_gray)
mask[:] = 255
for x, y in [np.int32(tr[-1]) for tr in self.tracks]:
cv.circle(mask, (x, y), 5, 0, -1)
p = cv.goodFeaturesToTrack(frame_gray, mask = mask, **feature_params)
if p is not None:
for x, y in np.float32(p).reshape(-1, 2):
self.tracks.append([(x, y)])
self.frame_idx += 1
self.prev_gray = frame_gray
cv.imshow('lk_track', vis)
ch = cv.waitKey(1)
if ch == 27:
break
# 加上这一句避免卡死
self.cam.release()
def main():
import sys
try:
# video_src = sys.argv[1]
video_src ='slow_traffic_small.mp4'
except:
video_src = 0
App(video_src).run()
print('Done')
if __name__ == '__main__':
print(__doc__)
main()
cv.destroyAllWindows()
复制代码
Lucas-Kanade tracker
====================
Lucas-Kanade sparse optical flow demo. Uses goodFeaturesToTrack
for track initialization and back-tracking for match verification
between frames.
Usage
-----
lk_track.py [<video_source>]
Keys
----
ESC - exit
Done
复制代码
三、OpenCV中的密集光流
Lucas-Kanade 方法计算稀疏特征集的光流(在我们的示例中,使用 Shi-Tomasi 算法检测到的角)。 OpenCV 提供了另一种算法来寻找密集的光流。 它计算帧中所有点的光流。 它基于 Gunnar Farneback 的算法,该算法在 Gunnar Farneback 在 2003 年的“基于多项式展开的双帧运动估计”中进行了解释。下面的示例显示了如何使用上述算法找到密集光流。 我们得到一个带有光流向量 (u,v) 的 2 通道阵列。 我们找到了它们的大小和方向。 我们对结果进行颜色编码以获得更好的可视化效果。 方向对应于图像的色调值。 幅度对应于价值平面。 请看下面的代码:
- cv.calcOpticalFlowFarneback 参数解析:
-
prevImg: 前一帧8-bit单通道图像
-
nextImg: 当前帧图像,与前一帧保持同样的格式、尺寸
-
pyr_scale: 金字塔上下两层之间的尺度关系,该参数一般设置为pyrScale=0.5,表示图像金字塔上一层是下一层的2倍降采样
-
levels:图像金字塔的层数
-
winsize:均值窗口大小,winsize越大,算法对图像噪声越鲁棒,并且能提升对快速运动目标的检测效果,但也会引起运动区域模糊。
-
iterations:算法在图像金字塔每层的迭代次数
-
poly_n:用于在每个像素点处计算多项式展开的相邻像素点的个数。poly_n越大,图像的近似逼近越光滑,算法鲁棒性更好,也会带来更多的运动区域模糊。通常,poly_n=5 or 7
-
poly_sigma:标准差,poly_n=5时,poly_sigma = 1.1;poly_n=7时,poly_sigma = 1.5
-
flags:Operation flags that can be a combination of the following:
- OPTFLOW_USE_INITIAL_FLOW 使用输入光流作为初始化光流近似
- OPTFLOW_FARNEBACK_GAUSSIAN 使用高斯滤波器代替相同大小的盒式滤波器进行光流估计。 通常,与箱式过滤器相比,此选项可提供更精确的 z 流量,但会降低速度。 通常,应将高斯窗口的 winsize 设置为更大的值,以实现相同级别的鲁棒性。
import numpy as np
import cv2 as cv
cap = cv.VideoCapture(cv.samples.findFile("vtest.avi"))
ret, frame1 = cap.read()
prvs = cv.cvtColor(frame1, cv.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 = cv.cvtColor(frame2, cv.COLOR_BGR2GRAY)
# 传入参数为:前一帧灰度图,当前帧,【结果光流】,相邻金字塔尺度关系(上面是下层的多少倍),
# 金字塔层数,均值窗口大小,算法在图像金字塔的每层的迭代次数,...
flow = cv.calcOpticalFlowFarneback(prvs, next, None, 0.5, 3, 15, 3, 5, 1.2, 0)
# 从得到的光流结果中分理出光强和色调角度
mag, ang = cv.cartToPolar(flow[..., 0], flow[..., 1])
# 弧度转角度
hsv[..., 0] = ang*180/np.pi/2
# 光强规范化
hsv[..., 2] = cv.normalize(mag, None, 0, 255, cv.NORM_MINMAX)
bgr = cv.cvtColor(hsv, cv.COLOR_HSV2BGR)
# 组合显示
res = np.hstack([frame2, bgr])
cv.imshow('Result', res)
k = cv.waitKey(30) & 0xff
if k == 27:
break
elif k == ord('s'):
cv.imwrite('opticalfb.png', frame2)
cv.imwrite('opticalhsv.png', bgr)
prvs = next
cv.destroyAllWindows()
复制代码