背景分离

背景分离(BS)是一种通过使用静态相机来生成前景掩码(即包含属于场景中的移动对象像
素的二进制图像)的常用技术。BS计算前景掩码,在当前帧与背景模型之间执行减法运算。

背景建模包括两个主要步骤: 1. 背景初始化; 2. 背景更新。

第一步,计算背景的初始模型,
第二步,更新模型以适应场景中可能的变化。

代码主要步骤:

  1. 使用cv.VideoCapture从视频或图像序列中读取数据;
  2. 通过使用cv.BackgroundSubtractor类创建和更新背景类;
  3. 通过使用cv.imshow获取并显示前景蒙版;
import cv2 as cv
import argparse
parser = argparse.ArgumentParser(description='This program shows how to use background subtraction methods provided byOpenCV. You can process both videos and images.')
parser.add_argument('--input', type=str, help='Path to a video or a sequence of image.', default='1.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(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

    # 更新背景模型
    fgMask = backSub.apply(frame)

    # 展示当前帧和背景掩码
    cv.imshow('Frame', frame)
    cv.imshow('FG Mask', fgMask)
    keyboard = cv.waitKey(30)
    if keyboard == 'q' or keyboard == 27:
        break

MOG2方法时视频第10帧背景掩码输出:

KNN方法时,视频第10帧掩码输出:

光流

光流是由物体或照相机的运动引起的两个连续帧之间图像物体的视运动的模式。它是2D向量场,
其中每个向量都是位移向量,表示点从第一帧到第二帧的运动。

下图显示了一个球连续5帧运动。箭头显示其位移向量:

光流基于以下两个假设:

  1. 在连续的帧之间,对象的像素强度不变。
  2. 相邻像素具有相似的运动。

考虑第一帧中的像素 I ( x , y , t ) I(x,y ,t) I(x,y,t) t t t代表时间维度)。它在 d t d_t dt时间之后拍摄的下一帧中按距离 ( d x , d y ) (d_x,d_y) (dxdy)移动。因此,由于这些像素相同且强度不变,可以认为:
I ( x , y , t ) = I ( x + d x , y + d y , t + d t ) 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)
使用泰勒级数的右侧逼近,去掉常项并除以 d t d_t dt得到下面的式子:
f x u + f y v + f t = 0 f_x u + f_y v + 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=xf;fy=yf;u=dtdx;v=dtdy
上式方程即为光流方程式。 f x f_x fx f y f_y fy是已知的,是图像渐变。 f t f_t ft是时间梯度,未知的就是 u , v u,v uv

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)。问题就变成了求解具有两个未知变量的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]=[ifxi2ifxifyiifxifyiifyi2]1[ifxiftiifyifti]
这个想法存在局限性,当运动超过一定幅度的时候,假设就不成立了。当然为了解决这个问题还有lucas金字塔方法来缓解这一问题。

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=' ')
parser.add_argument('--image', type=str, default='1.avi')
args = parser.parse_args()
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))
# 创建一些随机的颜色
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, (a,b),(c,d), color[i].tolist(), 2)
        frame = cv.circle(frame,(a,b),5,color[i].tolist(),-1)
    img = cv.add(frame,mask)
    cv.imshow('frame',img)
    k = cv.waitKey(30) & 0xff
    if k == 27:
        break
        # 现在更新之前的帧和点
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1, 1, 2)

效果:

OpenCV中的密集光流

除了Lucas-Kanade光流法外,opencv中还有一种密集光流算法。它基于Gunner Farneback算法计算帧中所有点的光通量。

首先获得一个带有光流矢量 ( u , v (u,v (u,v)的2通道阵列,找到了它们的大小和方向。对结果进行颜色编码,以实现更好的可视化。方向对应于图像的色相值,幅度对应于值平面。请参见下面的代码:

import numpy as np
import cv2 as cv
cap = cv.VideoCapture(cv.samples.findFile("1.avi"))  #获取视频
ret, frame1 = cap.read()
prvs = cv.cvtColor(frame1,cv.COLOR_BGR2GRAY)   #转变灰度图
hsv = np.zeros_like(frame1)
hsv[:,:,1] = 255  #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)  #使用Gunnar Farneback的算法计算密集光流。

    mag, ang = cv.cartToPolar(flow[...,0], flow[...,1])  #用于计算2维向量的大小和角度
    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:
        break
    elif k == ord('s'):
        cv.imwrite('opticalfb.png',frame2)
        cv.imwrite('opticalhsv.png',bgr)
    prvs = next   #更新帧