1.原理介绍
1.1 模型工作原理
- 背景物体就是指静止的或是非常缓慢的移动的物体,而前景物体就对应移动的物体。
- 所以我们可以把物体检测看出一个分类问题,也就是来确定一个像素点是否属于背景点。
- 在ViBe模型中,背景模型为每个像素点存储了一个样本集,然后将每一个新的像素值和样本集进行比较来判断是否属于背景点。
可以知道如果一个新的观察值属于背景点那么它应该和样本集中的采样值比较接近。该模型主要包括三个方面:
- (1)算法模型初始化;
- (2)像素的分类过程;
- (3)模型的更新策略。
1.2关于样本集的大小
假定我们要处理的每一帧图像是$M \times N$ 个像素的,$x$表示某帧图像的一个像素点。模型要为$M \times N$中每个像素建立一个样本集,$x$像素的样本集可以表示为
$$ M(x)=\{p_1 , p_2 , p_3 … p_n\} $$
每个样本集的大小为n,n这个值如何确定的,暂时不用管,一般是实验得出的,论文中取$n=20$。
所以样本集的总大小为$M \times N \times n$。
1.3模型的初始化
初始化就是建立背景模型的过程。通用的检测算法的初始化需要一定长度的视频序列来完成,通常要耗费数秒的时间,这极大的影戏的检测的实时性,对于手持相机实时拍照来讲并不合适。
ViBe的初始化仅仅通过一帧图像即可完成。ViBe初始化就是填充像素的样本集的过程。由于在一帧图像中不可能包含像素点的时空分布信息,我们利用了相近像素点拥有相近的时空分布特性,具体来讲就是:
对于一个像素点$x$,随机的选择它的邻居点$NG(x)$的像素值作为它的模型样本值$M_0(x)$。
$$ M_0(x) = {p_0(y | y ∈NG(x))} $$
这种初始化方法的优缺点:
优点
- 对于噪声的反应比较灵敏,
- 计算量小速度快,
- 不仅减少了背景模型建立的过程,还可以处理背景突然变化的情况,当检测到背景突然变化明显时,只需要舍弃原始的模型,重新利用变化后的首帧图像建立背景模型。
缺点
- 用于作平均的几帧初始图像中可能采用了运动物体的像素,这种条件下初始化样本集,容易引入拖影(Ghost)区域;
1.4像素的分类过程(前景检测)
如下图,假定当前帧为第$ t$ 帧,$p_t(x)$表示第$ t$ 帧图像 $x$ 像素的像素值,图中的$p1$到$p6$都是$x$像素的样本集中的值。
那图中的横坐标C1和纵坐标C2是什么呢?我们假定我们处理的图像每个像素是RGB格式的,即一个像素值由R,G,B三个值表示,那么图中的坐标其实还隐藏了C3,即C1,C2,C3表示的正是三个通道值,如果是灰度图的话下面的图就要画成一维一条直线了。
接下来我们根据预先设定的半径R统计以当前像素点为中心的圆形区域$S_R(p_t)$(实际对应到RGB空间中为球形区域)中包含的该像素点的样本集中的样本的数量,这里记为#。
- 在距$p_t(x)$值半径R距离范围内的样本值有$p_2,p_4$,在半径R范围内的样本值总数计为#,那么下图#=2。
- 让后将该值与预先设定的阈值#min(论文中给出的值是2)进行对比,当#<#min的值时,x这个像素就被标记为前景像素,否则就将其标记为背景像素,依次处理所有像素,就能得出前景图像了
1.5模型的更新策略
即使已经建立起了背景模型,也应该对背景模型进行不断的更新,这样才能使得背景模型能够适应背景的不断变化(如光照变化,背景物体变更等)。
A. 普通更新策略
对于其他的背景提取算法,背景模型有两种不同的更新策略:
保守更新策略:前景点永远不会用来填充模型
- 这样会引起死锁,产生Ghost区域。比如初始化的时候如果一块静止的区域被错误的检测为运动的,那么在这种策略下它永远会被当做运动的物体来对待;
Blind策略:前景和背景都可以用来更新背景模型;
- 对死锁不敏感,但这样的缺点在于,缓慢移动的物体会融入到背景中,无法检测出来;
B. ViBe算法更新策略
ViBe算法中,使用的更新策略是:保守更新策略 + 前景点计数法 + 随机子采样。
- 保守更新策略:前景点永远不会用来填充模型
- 前景点计数法:对像素点进行统计,如果某个像素点连续N次被检测为前景,则将其更新为背景点;
随机子采样:在每一个新的视频帧中都去更新背景模型中的每一个像素点的样本值是没有必要的,当一个像素点被分类为背景点时,它有1/φ的概率去更新背景模型。
这就决定了ViBe算法的更新策略的其他属性:
- 无记忆更新策略:每次确定需要更新像素点的背景模型时,以新的像素值随机取代该像素点样本集的一个样本值;
时间取样更新策略:并非每处理一帧数据,都需要更新处理,而是按一定的更新率更新背景模型;
- 当一个像素点被判定为背景时,它有1/φ的概率更新背景模型;
- φ是时间采样因子,一般取值为16;
- 空间邻域更新策略:针对需要更新像素点,在该像素点的邻域中随机选择一个像素点,以新选择的像素点更新被选中的背景模型;
C. ViBe算法具体更新的方法:
- 每个背景点都有1/φ的概率更新该像素点的模型样本值;
- 有1/φ的概率去更新该像素点邻居点的模型样本值;
- 前景点计数达到临界值时,将其变为背景,并有1/ φ的概率去更新自己的模型样本值。
2.算法实现
import numpy as np
import cv2
class ViBe:
'''
ViBe运动检测,分割背景和前景运动图像
'''
def __init__(self,num_sam=20,min_match=2,radiu=20,rand_sam=16):
self.defaultNbSamples = num_sam #每个像素的样本集数量,默认20个
self.defaultReqMatches = min_match #前景像素匹配数量,如果超过此值,则认为是背景像素
self.defaultRadius = radiu #匹配半径,即在该半径内则认为是匹配像素
self.defaultSubsamplingFactor = rand_sam #随机数因子,如果检测为背景,每个像素有1/defaultSubsamplingFactor几率更新样本集和领域样本集
self.background = 0
self.foreground = 255
def __buildNeighborArray(self,img):
'''
构建一副图像中每个像素的邻域数组
参数:输入灰度图像
返回值:每个像素9邻域数组,保存到self.samples中
'''
height,width=img.shape
self.samples=np.zeros((self.defaultNbSamples,height,width),dtype=np.uint8)
#生成随机偏移数组,用于计算随机选择的邻域坐标
ramoff_xy=np.random.randint(-1,2,size=(2,self.defaultNbSamples,height,width))
#ramoff_x=np.random.randint(-1,2,size=(self.defaultNbSamples,2,height,width))
#xr_=np.zeros((height,width))
xr_=np.tile(np.arange(width),(height,1))
#yr_=np.zeros((height,width))
yr_=np.tile(np.arange(height),(width,1)).T
xyr_=np.zeros((2,self.defaultNbSamples,height,width))
for i in range(self.defaultNbSamples):
xyr_[1,i]=xr_
xyr_[0,i]=yr_
xyr_=xyr_+ramoff_xy
xyr_[xyr_<0]=0
tpr_=xyr_[1,:,:,-1]
tpr_[tpr_>=width]=width-1
tpb_=xyr_[0,:,-1,:]
tpb_[tpb_>=height]=height-1
xyr_[0,:,-1,:]=tpb_
xyr_[1,:,:,-1]=tpr_
#xyr=np.transpose(xyr_,(2,3,1,0))
xyr=xyr_.astype(int)
self.samples=img[xyr[0,:,:,:],xyr[1,:,:,:]]
def ProcessFirstFrame(self,img):
'''
处理视频的第一帧
1、初始化每个像素的样本集矩阵
2、初始化前景矩阵的mask
3、初始化前景像素的检测次数矩阵
参数:
img: 传入的numpy图像素组,要求灰度图像
返回值:
每个像素的样本集numpy数组
'''
self.__buildNeighborArray(img)
self.fgCount=np.zeros(img.shape) #每个像素被检测为前景的次数
self.fgMask=np.zeros(img.shape) #保存前景像素
def Update(self,img):
'''
处理每帧视频,更新运动前景,并更新样本集。该函数是本类的主函数
输入:灰度图像
'''
height,width=img.shape
#计算当前像素值与样本库中值之差小于阀值范围RADIUS的个数,采用numpy的广播方法
dist=np.abs((self.samples.astype(float)-img.astype(float)).astype(int))
dist[dist<self.defaultRadius]=1
dist[dist>=self.defaultRadius]=0
matches=np.sum(dist,axis=0)
#如果大于匹配数量阀值,则是背景,matches值False,否则为前景,值True
matches=matches<self.defaultReqMatches
self.fgMask[matches]=self.foreground
self.fgMask[~matches]=self.background
#前景像素计数+1,背景像素的计数设置为0
self.fgCount[matches]=self.fgCount[matches]+1
self.fgCount[~matches]=0
#如果某个像素连续50次被检测为前景,则认为一块静止区域被误判为运动,将其更新为背景点
fakeFG=self.fgCount>50
matches[fakeFG]=False
#此处是该更新函数的关键
#更新背景像素的样本集,分两个步骤
#1、每个背景像素有1/self.defaultSubsamplingFactor几率更新自己的样本集
##更新样本集方式为随机选取该像素样本集中的一个元素,更新为当前像素的值
#2、每个背景像素有1/self.defaultSubsamplingFactor几率更新邻域的样本集
##更新邻域样本集方式为随机选取一个邻域点,并在该邻域点的样本集中随机选择一个更新为当前像素值
#更新自己样本集
upfactor=np.random.randint(self.defaultSubsamplingFactor,size=img.shape) #生成每个像素的更新几率
upfactor[matches]=100 #前景像素设置为100,其实可以是任何非零值,表示前景像素不需要更新样本集
upSelfSamplesInd=np.where(upfactor==0) #满足更新自己样本集像素的索引
upSelfSamplesPosition=np.random.randint(self.defaultNbSamples,size=upSelfSamplesInd[0].shape) #生成随机更新自己样本集的的索引
samInd=(upSelfSamplesPosition,upSelfSamplesInd[0],upSelfSamplesInd[1])
self.samples[samInd]=img[upSelfSamplesInd] #更新自己样本集中的一个样本为本次图像中对应像素值
#更新邻域样本集
upfactor=np.random.randint(self.defaultSubsamplingFactor,size=img.shape) #生成每个像素的更新几率
upfactor[matches]=100 #前景像素设置为100,其实可以是任何非零值,表示前景像素不需要更新样本集
upNbSamplesInd=np.where(upfactor==0) #满足更新邻域样本集背景像素的索引
nbnums=upNbSamplesInd[0].shape[0]
ramNbOffset=np.random.randint(-1,2,size=(2,nbnums)) #分别是X和Y坐标的偏移
nbXY=np.stack(upNbSamplesInd)
nbXY+=ramNbOffset
nbXY[nbXY<0]=0
nbXY[0,nbXY[0,:]>=height]=height-1
nbXY[1,nbXY[1,:]>=width]=width-1
nbSPos=np.random.randint(self.defaultNbSamples,size=nbnums)
nbSamInd=(nbSPos,nbXY[0],nbXY[1])
self.samples[nbSamInd]=img[upNbSamplesInd]
def getFGMask(self):
'''
返回前景mask
'''
return self.fgMask
- 调用测试
import matplotlib.pyplot as plt
# 使用opencv加载目标视频
video_path = "./test.mp4"
capture = cv2.VideoCapture(video_path)
# 实例化ViBe模型
vibe=ViBe()
# 第一帧标志
flag_first_frame = True
while True:
# 逐一读取视频帧
ret, frame = capture.read()
if not ret:
break
# 将视频帧转为灰度图
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 如果是第一帧,则用于初始化背景模型
if flag_first_frame:
vibe.ProcessFirstFrame(gray)
flag_first_frame = False
continue
# 否则更新背景模型
vibe.Update(gray)
# 获取前景并转为uint8类型
segMat=vibe.getFGMask()
segMat = segMat.astype(np.uint8)
# 拼接出显示图片
img_show = np.hstack((frame,cv2.cvtColor(segMat,cv2.COLOR_GRAY2BGR)))
# 缩放到原来的二分之一并显示
x, y = img_show.shape[0:2]
img_show = cv2.resize(img_show, (int(y / 2), int(x / 2)))
cv2.imshow('vibe',img_show)
if cv2.waitKey(50)&0xFF==ord("q"):
break
# 释放资源
capture.release()
cv2.destroyAllWindows()
评论 (0)