目录
如何使用背景分离方法
- 背景分离(BS)是一种通过使用静态相机来生成前景掩码(即包含属于场景中的移动对象像素的二进制图像)的常用技术。
- BS计算前景掩码,在当前帧与背景模型之间执行减法运算,其中包含场景的静态部分,或者更一般而言,考虑到所观察场景的特征,可以将其视为背景的所有内容。
背景建模包括两个主要步骤: 1. 背景初始化; 2. 背景更新。
第一步,计算背景的初始模型,第二步,更新模型以适应场景中可能的变化。
OpenCV中的BS
cv::BackgroundSubtractor
对象用于生成前景掩码。在下面示例中,使用了默认参数,但是也可以在create函数中声明特定的参数。
cv::VideoCapture
对象用于读取输入视频或输入图像序列。- 每帧都用于计算前景掩码和更新背景。如果要更改用于更新背景模型的学习率,可以通过将参数传递给apply方法来设置特定的学习率。
- 当前帧号可以从
cv::VideoCapture
对象中提取,并标记在当前帧的左上角,白色矩形用于突出显示黑色的帧编号。最后显示当前的输入框和结果。
下面示例bs.py,将让用户选择处理视频文件或图像序列,使用cv::BackgroundSubtractorMOG2
生成前景掩码。
from __future__ import print_function
import cv2 as cv
import argparse
parser = argparse.ArgumentParser(description='This program shows how\
to use background subtraction methods provided by \
OpenCV. You can process both videos and images.')
parser.add_argument('--input', type=str,
help='Path to a video or a sequence of image.', default='./OpenCV/data/vtest.avi')
parser.add_argument('--algo', type=str,
help='Background subtraction method (KNN, MOG2).', default='MOG2')
args = parser.parse_args()
if args.algo == 'MOG2':
backSub = cv.createBackgroundSubtractorMOG2()
else:
backSub = cv.createBackgroundSubtractorKNN()
capture = cv.VideoCapture(cv.samples.findFileOrKeep(args.input))
if not capture.isOpened:
print('Unable to open:' + args.input)
exit(0)
while True:
ret, frame = capture.read()
if frame is None:
break
if str(capture.get(cv.CAP_PROP_POS_FRAMES)) == '223.0':
cv.waitKey(0)
fgMask = backSub.apply(frame)
cv.rectangle(frame, (10, 2), (100, 20), (255, 255, 255), -1)
cv.putText(frame, str(capture.get(cv.CAP_PROP_POS_FRAMES)), (15, 15),
cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0))
cv.imshow('Frame', frame)
cv.imshow('FG Mask', fgMask)
k = cv.waitKey(30)
if k == ord('q') or k == 27:
break
使用MOG2方法,程序输出如下所示(检测到灰色区域有阴影):
使用KNN方法,程序的输出将如下所示(检测到灰色区域的阴影):
Meanshift和Camshift
Meanshift
Meanshift背后的直觉很简单,假设有点的集合(它可以是像素分布,例如直方图反投影)。会得到一个小窗口(可能是一个圆形),并且必须将该窗口移到最大像素密度(或最大点数)的区域。如下图所示:
初始窗口以蓝色圆圈显示,名称为“C1”,其原始中心以蓝色矩形标记,名称为“C1_o”;但是,如果找到该窗口内点的质心,则会得到点“C1_r”(标记为蓝色小圆圈),它是窗口的真实质心,当然,它们不匹配。因此,移动窗口,使新窗口的圆与上一个质心匹配;再次找到新的质心,很可能不会匹配。因此,再次移动它,并继续迭代,以使窗口的中心及其质心落在同一位置(或在很小的期望误差内)。因此,最终获得的是一个具有最大像素分布的窗口,它带有一个绿色圆圈,名为“C2”。正如在图像中看到的,它具有最大的点数。
整个过程在下面的静态图像上演示:
通常会传递直方图反投影图像和初始目标位置。当对象移动时,显然该移动会反映在直方图反投影图像中,meanshift算法将窗口移动到最大密度的新位置。
OpenCV中的Meanshift
要在OpenCV中使用meanshift,首先需要设置目标,找到其直方图,以便可以将目标反投影到每帧上以计算均值偏移。还需要提供窗口的初始位置,对于直方图,此处仅考虑色相。另外,为避免由于光线不足而产生错误的值,可以使用cv.inRange()
函数丢弃光线不足的值。
meanshift.py
import cv2 as cv
import numpy as np
import argparse
parser = argparse.ArgumentParser(description='This sample demonstrates the meanshift algorithm.')
parser.add_argument('--image', type=str,
help='Path to image file.', default='./OpenCV/data/slow_traffic_small.mp4')
args = parser.parse_args()
cap = cv.VideoCapture(args.image)
# 视频的第一帧
ret, frame = cap.read()
# 设置窗口的初始位置
x, y, w, h = 300, 200, 100, 50
track_window = (x, y, w, h)
# 设置初始ROI来追踪
roi = frame[y: y + h, x : x + w]
hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
mask = cv.inRange(hsv_roi, np.array((0., 60., 32.)), np.array((180., 255., 255.)))
roi_hist = cv.calcHist([hsv_roi], [0], mask, [180], [0, 180])
cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX)
# 设置中止条件,可以是10次迭代,也可以至少移动1 pt
term_crit = (cv.TermCriteria_EPS | cv.TermCriteria_COUNT, 10, 1)
while(1):
ret, frame = cap.read()
if ret == True:
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
# 应用meanshift来获取新位置
ret, track_window = cv.meanShift(dst, track_window, term_crit)
# 在图像上绘制
x, y, w, h = track_window
img2 = cv.rectangle(frame, (x, y), (x + w, y + h), 255, 2)
cv.imshow('img2', img2)
k = cv.waitKey(30) & 0xff
if k == 27 or k == ord('q'):
break
else:
break
Camshift
观察最后结果,存在一个问题,无论汽车离相机很近或非常近,窗口始终具有相同的大小。这是不好的,需要根据目标的大小和旋转来调整窗口大小。该解决方案再次来自“ OpenCV Labs”,它被称为Gary布拉德斯基(Gary Bradsky)在其1998年的论文“用于感知用户界面中的计算机视觉面部跟踪”中发表的CAMshift(连续自适应均值偏移)[26]。 它首先应用Meanshift,一旦Meanshift收敛,它将更新窗口的大小为
s
=
2
×
M
00
256
s = 2 \times \sqrt{\frac{M_{00}}{256}}
s=2×256M00。它还可以计算出最合适的椭圆的方向,再次将均值偏移应用于新的缩放搜索窗口和先前的窗口位置,该过程一直持续到达到要求的精度为止。
OpenCV中的Camshift
它与meanshift相似,但是返回一个旋转的矩形(即结果)和box参数(用于在下一次迭代中作为搜索窗口传递)。
camshift.py
只需修改meanshift中部分代码即可
ret, track_window = cv.CamShift(dst, track_window, term_crit)
光流
光流的概念
光流是由物体或照相机的运动引起的两个连续帧之间图像物体的视运动的模式。它是2D向量场,其中每个向量都是位移向量,表示点从第一帧到第二帧的运动。考虑下面的图片(图片提供:Wikipedia关于Optical Flow的文章)。
它显示了一个球连续5帧运动,箭头显示其位移向量。光流在以下领域具有许多应用: - 运动的结构 - 视频压缩 - 视频稳定…
光流基于以下几个假设进行工作: 1. 在连续的帧之间,对象的像素强度不变。 2. 相邻像素具有相似的运动。
考虑第一帧中的像素
I
(
x
,
y
,
t
)
I(x, y, t)
I(x,y,t)(在此处添加新维度:时间。之前只处理图像,因此不需要时间)。它在
d
t
dt
dt时间之后拍摄的下一帧中按距离
(
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
f_x = \frac{\partial f}{\partial x} \; ; \; f_y = \frac{\partial f}{\partial y}
fx=∂x∂f;fy=∂y∂f
u
=
d
x
d
t
;
v
=
d
y
d
t
u = \frac{dx}{dt} \; ; \; v = \frac{dy}{dt}
u=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方法在该点周围需要3x3色块,因此,所有9个点都具有相同的运动,可以找到这9点的
(
f
x
,
f
y
,
f
t
)
(f_x, f_y, f_t)
(fx,fy,ft)。所以现在的问题变成了求解带有两个未知变量的9个方程组的问题。用最小二乘拟合法可获得更好的解决方案。下面是最终的解决方案,它是两个方程式-两个未知变量问题,求解以获得解决答案。
[
u
v
]
=
[
∑
i
f
x
i
2
∑
i
f
x
i
f
y
i
∑
i
f
x
i
f
y
i
∑
i
f
y
i
2
]
−
1
[
−
∑
i
f
x
i
f
t
i
−
∑
i
f
y
i
f
t
i
]
\begin{bmatrix} u \\ v \end{bmatrix} = \begin{bmatrix} \sum_{i}{f_{x_i}}^2 & \sum_{i}{f_{x_i} f_{y_i} } \\ \sum_{i}{f_{x_i} f_{y_i}} & \sum_{i}{f_{y_i}}^2 \end{bmatrix}^{-1} \begin{bmatrix} - \sum_{i}{f_{x_i} f_{t_i}} \\ - \sum_{i}{f_{y_i} f_{t_i}} \end{bmatrix}
[uv]=[∑ifxi2∑ifxifyi∑ifxifyi∑ifyi2]−1[−∑ifxifti−∑ifyifti]
(用哈里斯拐角检测器检查逆矩阵的相似性,这表示拐角是更好的跟踪点。)因此,从用户的角度来看,这个想法很简单,给一些跟踪点,接收到这些光流矢量点。但是同样存在一些问题,到现在为止,只处理小动作,所以当大动作时它就失败了。为了解决这个问题,使用金字塔。当上金字塔时,较小的动作将被删除,较大的动作将变为较小的动作。因此,通过在此处应用Lucas-Kanade,可以获得与尺度一致的光流。
OpenCV中的Lucas-Kanade
OpenCV在单个函数cv.calcOpticalFlowPyrLK()
中提供所有这些功能。在这里,创建一个简单的应用程序来跟踪视频中的某些点。为了确定点,使用cv.goodFeaturesToTrack()
。采用第一帧,检测其中的一些Shi-Tomasi角点,然后使用Lucas-Kanade光流迭代地跟踪这些点。对于函数cv.calcOpticalFlowPyrLK()
,传递前一帧,前一点和下一帧。它返回下一个点以及一些状态码,如果找到下一个点,状态码的值为1,否则为零。将这些下一个点迭代地传递为下一步中的上一个点。请参见下面的代码lucas_kanade.py:
import cv2 as cv
import numpy as np
import argparse
parser = argparse.ArgumentParser(description='This sample demonstrates Lucas-Kanade Optical Flow calculation.')
parser.add_argument('--image', type=str,
help='Path to image file.', default='./OpenCV/data/slow_traffic_small.mp4')
args = parser.parse_args()
cap = cv.VideoCapture(args.image)
# 用于ShiTomasi拐点检测的参数
feature_params = dict(maxCorners = 100, qualityLevel = 0.3,
minDistance = 7, blockSize = 7)
# Lucal-Kanade光流参数
lk_params = dict(winSize = (15, 15), maxLevel = 2,
criteria = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03))
# 创建一些随机的颜色
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()
frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
# 计算光流
p1, st, err = cv.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 = 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 or k == ord('q'):
break
# 现在更新之前的帧和点
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1, 1, 2)
此代码不会检查下一个关键点的正确性。因此,即使任何特征点在图像中消失了,光流也有可能找到下一个看起来可能与它接近的下一个点。因此,对于稳健的跟踪,实际上 应该以特定的时间间隔检测点。OpenCV样本附带了这样一个样本,该样本每5帧发现一次特征点,并且还对光流点进行了后向检查,以仅选择良好的流点。请参阅代码lk_track.py
OpenCV中的密集光流
Lucas-Kanade方法计算稀疏特征集的光流(在示例中为使用Shi-Tomasi算法检测到的角)。OpenCV提供了另一种算法来查找密集的光流,它计算帧中所有点的光通量。它基于Gunner Farneback的算法,在2003年Gunner Farneback的“基于多项式展开的两帧运动估计”中对此进行了解释。
下面的示例显示了如何使用上述算法找到密集的光流。得到一个带有光流矢量
(
u
,
v
)
(u, v)
(u,v)的2通道阵列,找到了它们的大小和方向,对结果进行颜色编码,以实现更好的可视化。方向对应于图像的色相值,幅度对应于值平面。请参见下面的代码dense_optical.py:
import cv2 as cv
import numpy as np
cap = cv.VideoCapture(cv.samples.findFile('./OpenCV/data/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()
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)
cv.imshow('frame2', bgr)
k = cv.waitKey(30) & 0xff
if k == 27 or k == ord('q'):
break
elif k == ord('s'):
cv.imwrite('./OpenCV/opticalfb.png', frame2)
cv.imwrite('./OpenCV/opticalhsv.png', bgr)
prvs = next
学习来源:OpenCV-Python中文文档