目录

OpenCV Chapter7 背景建模与光流估计


OpenCV

背景建模

帧差法

由于场景中的目标在运动,目标的影像在不同图像帧中的位置不同,该类算法对时间上连续的两帧图像进行差分运算,不同帧对应的像素点相减,判断灰度差的绝对值,当绝对值超过一定阈值时,即可判断为运动目标,从而实现目标的检测功能 $$ \begin{align} &D_n=|f_n(x,y)-f_{n-1}(x,y)|\newline &R_n(x,y)=\begin{cases} &255,D_n(x,y) > T\newline &0,else\newline \end{cases} \end{align} $$ 帧差法非常简单,但是会引入噪音和空洞(只能判断外部)问题

混合高斯模型

在进行前景检测前,先对背景进行训练,对图像中每个背景采用一个混合高斯模型进行模拟,每个背景的混合高斯模型的个数可以自适应,然后在测试阶段,对新来的像素进行$GMM$匹配,如果该像素能够匹配其中一个高斯,则认为时背景,否则认为是前景。由于整个过程$GMM$模型在不断更新学习中,所以对动态背景有一定的鲁棒性。最后通过对一个有树枝摇摆的动态背景进行前景检测,取得很好的效果

在视频中对于像素点的变化情况应当是符合高斯分布

背景的实际分布应当是和多个高斯分布混合在一起,每个高斯模型也可以带有权重

混合高斯模型学习方法

  • 首先初始化每个高斯模型矩阵参数
  • 取视频中的$T$帧数据图像用来训练高斯混合模型,来了第一个像素之后用它来当做第一个高斯分布
  • 当后面来的像素值时,与前面已有的高斯均值比较,如果该像素点的值与其模型的均值差在3倍的方差内,则属于该分布,并对其进行参数更新
  • 如果下一次来的像素不满足当前高斯分布,用它来创建一个新的高斯分布

最后会生成多个高斯分布,但是最后只会保留3-5个

混合高斯模型测试方法

在测试阶段,对新来像素点的值与混合高斯模型中的每一个均值进行比较,如果其差值在2倍的方差之间的话,则认为是背景,否则认为是前景。将前景赋值为255,背景赋值为0。这样就形成了一幅前景二值图。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import cv2

video = cv2.VideoCapture("video path")

# 形态学操作需要使用
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))

# 创建混合高斯模型用于背景建模
fgbg = cv2.createBackgroundSubtractorMOG2()

while True:
    ret, frame = video.read()
    if frame is None:
        break
    fgmask = fgbg.apply(frame)  # 混合高斯模型应用
    # 形态学开运算去噪点
    fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel)
    # 寻找视频中的轮廓
    contours, hierachy = cv2.findContours(fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    for c in contours:
        perimeter = cv2.contourArea(c)
        if perimeter > 3000:
            # 找到一个直矩形
            x, y, w, h = cv2.boundingRect(c)
            # 画出矩形
            cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 2)

    cv2.imshow('frame', frame)
    cv2.imshow('fgmask', fgmask)
    k = cv2.waitKey(100) & 0xff
    if k == 27:
        break

video.release()
cv2.destroyAllWindows()

光流估计

光流是空间运动物体在观测成像平面上的像素运动的“瞬时速度”,根据各个像素点的速度矢量特征,可以对图像进行动态分析,例如目标跟踪

  • 亮度恒定:同一点随着时间的变化,其亮度不会发生改变
  • 小运动:随着时间的变化不会引起位置的剧烈变化,只有小运动的情况下才能用前后帧之间单位位置变化引起的灰度变化去近似灰度对位置的偏导数
  • 空间一致:一个场景上邻近的店投影到图像上也是邻近点,且邻近点速度一致。因为光流法基本方程约束只有一个,而要求x,y方向的速度,有两个未知变量,所以需要联立n多个方程求解

$Lucas-Kanade$算法

约束方程 $$ \begin{align} I(x,y,t)&=I(x+dx,y+dy,t+dt)\newline &=I(x,y,t)+\frac{\partial I}{\partial x}dx+\frac{\partial I}{\partial y}dy+\frac{\partial I}{\partial t}dt\newline \end{align} $$

$$ I_xdx+I_ydy+I_tdt=0\newline I_x\frac{dx}{dt}+I_y\frac{dy}{dt}=-I_t\newline \frac{dx}{dt}=u\ and\ \frac{dy}{dt}=v\newline I_xu+I_yv=-I_t\newline \rightarrow \begin{bmatrix}I_x\ I_y\end{bmatrix} \begin{bmatrix}u\newline v\end{bmatrix}=-I_t\newline $$

如何求解方程组呢?一个像素点不够,物体移动过程中还有那些特性?

空间一致$\rightarrow$周围点速度一致,可以增加方程组的数量

将一个$kernel$之中的所有点用来列方程组 $$ \begin{bmatrix} &I_{x1}\ I_{y1}\newline &I_{x2}\ I_{y2}\newline &\vdots \end{bmatrix} \begin{bmatrix} u\newline v\newline \end{bmatrix}= -\begin{bmatrix} &I_{t1}\newline &I_{t2}\newline &\vdots \end{bmatrix} $$ 最小二乘法(机器学习线性回归) $$ A\vec u=b\newline \downarrow\newline A^TA\vec u=A^Tb\newline \downarrow\newline \vec u=(A^TA)^{-1}A^Tb\newline A^{T}A= \begin{bmatrix} &\sum I_x^2\ &\sum I_xI_y\newline &\sum I_xI_y\ &\sum I_y^2\newline \end{bmatrix} $$ $A^TA$可逆条件,设两个特征值为$\lambda_1,\lambda_2$,两个值都比较大的时候可逆(角点也是这个判定条件),所以最后关注的点大部分都是角点

代码

1
cv2.calcOpticalFlowPyrLK(prevImg, nextImg, prevPts, nextPts, status=None, err=None, winSize=None, maxLevel=None, criteria=None, flags=None, minEigThreshold=None)

参数

  • prevImage:前一帧图像

  • nextImage:当前帧图像

  • prevPts:待跟踪的特征点向量

  • winSize:搜索窗口的大小

  • maxLevel:最大的金字塔层数

返回

  • nextPts:输出跟踪特征点向量
  • status:特征点是否找到,找到状态为1,未找到状态为0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import cv2
import numpy as np

video = cv2.VideoCapture("video path")

# 角点检测所需参数
feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7)

# lucas_kanade参数
lk_params = dict(winSize=(15, 15), maxLevel=2)

# 随机颜色条
color = np.random.randint(0, 255, (100, 3))

# 拿到第一帧图像
old_ret, old_frame = video.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
# 返回所有检测特征点,需要输入图像,角点最大数量(效率),品质因子(特征值越大的越好,来筛选)
# 距离相当于这区间有比这个角点强的,就不要这个弱的了
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)

# 创建一个mask
mask = np.zeros_like(old_frame)

while True:
    ret, frame = video.read()
    if frame is None:
        break
    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # 需要传入前一帧和当前图像以及前一帧检测到的角点
    p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)

    # st = 1表示
    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('frame', img)
    k = cv2.waitKey(50) & 0xff
    if k == 27:
        break

    # Update
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1, 1, 2)

video.release()
cv2.destroyAllWindows()