一.差分法
属于在静态背景下的目标检测方法,是实现运动估计的一种简单而直观的方法,主要基于连续帧之间的像素强度变化。差分法的核心思想是通过比较连续两帧或多帧图像之间的像素差异来估计运动。如果一个像素在连续的帧之间有显著的变化,那么可以推断该位置发生了运动。
1.帧间差分法
帧间差分法是通过对连续的相邻两帧或者三帧图像上的对应的像素值作差运算来检测运动目标的方法。
优点:算法实现简单,计算量不大且运行速度快,鲁棒性好,场景光线变化不敏感
缺点:会形成“空洞”现象,即运动物体内部可能出现空洞,由于物体内部的像素在连续帧间没有显著变化;帧间差分法可以检测运动,但无法提供关于运动速度或方向的信息;:对于快速移动的物体,帧间差分法可能无法准确检测整个运动区域
代码示例如下
import cv2
cap=cv2.VideoCapture('D:\\素材\\1.mp4')
#判断视频是否读取成功
if(cap.isOpened()==False):
print("Error opening")
frameNum=0 #记录帧数
while(cap.isOpened()):#循环视频中的每一帧
ret,frame=cap.read()#读取每一帧,ret表示读取是否成功,frame是读取的帧
frameNum+=1
if ret==True:
tempframe=frame #暂存当前帧
if(frameNum==1):
previousframe=cv2.cvtColor(tempframe,cv2.COLOR_BGR2GRAY)
if (frameNum>=2):
currentframe=cv2.cvtColor(tempframe,cv2.COLOR_BGR2GRAY)
currentframe=cv2.absdiff(currentframe,previousframe)#进行帧差法
#显示图片
cv2.imshow('原图',frame)
cv2.imshow('pre',previousframe)
cv2.imshow('cur',currentframe)
cv2.waitKey(0)
break
cap.release()#释放视频流
cv2.destroyAllWindows()#关闭所有窗口
2.背景差分法
它的核心思想是通过从当前帧中减去背景模型,从而检测出前景对象(通常是运动对象)。背景差分法对于静态摄像头捕获的视频尤其有效,因为背景可以被视为相对静态的,而任何显著的变化通常都归因于前景对象的运动。要点是需要先建立出背景模型
优点:对运动物体检测敏感;适用于静态背景场景
缺点:对背景变化敏感;动态背景处理困难;物体的阴影可能被误判为运动物体
代码示例如下
import cv2
cap=cv2.VideoCapture('D:\\素材\\1.mp4')
#判断视频是否读取成功
if(cap.isOpened()==False):
print("Error opening")
#使用高斯混合模型分离函数进行背景差分法
fgbg=cv2.createBackgroundSubtractorMOG2()
frameNum=0
while(cap.isOpened()):
ret,frame=cap.read()
frameNum+=1
fgmask=fgbg.apply(frame)
#显示图片
cv2.imshow('frame',fgmask)
cv2.imshow('original',frame)
cv2.waitKey(30)
if (frameNum==17):
cv2.imwrite('D:\\cg\\bck_fgmask.jpg',fgmask)
cv2.imwrite('D:\\cg\\bck_frame.jpg', frame)
break
cap.release()
cv2.destroyAllWindows()
分析可知背景差分法最关键的就是背景模型的建立,在不同的情况下选择不同的建模方式以此来取得更好的效果。以下介绍三种背景模型建立的方法
①中值法
在一段时间内取连续N帧视频图像序列,对于每个像素点,将这N帧图像在此点处N个像素值按从小到大排序,并将此中值作为该点背景图像中的灰度值。场景
优点:
- 鲁棒性强:不受噪声和光照变化的影响,能够有效地去除噪声和光照变化的影响。
- 简单易行:计算简单,易于实现。
缺点:
- 边缘模糊:容易造成边缘模糊,无法准确地提取目标的边缘。
- 计算量大:计算量较大,尤其是对于大图像而言。
代码示例如下
import cv2
import numpy as np
from PIL import Image
#以当前时刻帧为基准,即每次建模时,会使用当前帧的前m帧
m=19
#读取视频
capture=cv2.VideoCapture('D:\\素材\\1.mp4')
cap=cv2.VideoCapture('D:\\素材\\1.mp4')
#判断视频是否读取成功
if(cap.isOpened()==False):
print("Error opening")
#初始化一个空列表list,用于临时存储每个像素点在不同帧中的值,以便于后续进行排序和中值选取。
list=list()
def creatbackground(row,col,frameNum,number):
"""
:param row:视频帧行
:param col: 视频帧列
:param frameNum: 当前为第几帧
:param number:背景建模需要回溯的帧数
:return:背景建模图像矩阵
"""
#每次调用creatbackground函数开始时清空list,以便于新的像素值排序
list.clear()
#创建一个三维数组 frameArray,用于存储回溯帧的像素值。数组的维度是[number][row][col],对应于回溯帧数、行数和列数。
#k 从 0 到 number-1,用于遍历回溯帧
frameArray=[[[0 for j in range(col)] for i in range(row)] for k in range(number)]
#使用set方法将视频的读取位置设置到frameNum - number帧。这样做是为了回溯到当前帧的前number帧开始读取视频。
capture.set(cv2.CAP_PROP_POS_FRAMES,frameNum-number)
#这个循环读取回溯帧,并将每帧转换为灰度图像,然后将其保存到frameArray中。
# 这样,frameArray中存储了当前帧前number帧的灰度值。
for i in range(number):
ret,frame=capture.read()
frame=cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
frameArray[i]=np.array(frame)
#创建一个全零的二维数组background,其大小与视频帧相同。这个数组将用于存储计算出的背景模型。
background=np.zeros((row,col))
#通过三重循环遍历每个像素点,在回溯帧中对该像素点的值进行排序,然后取中值作为背景模型中该像素点的值
for i in range(row):
for j in range(col):
for k in range(number):
list.append(frameArray[k][i][j])
list.sort()
background[i][j]=list[int(number/2)]
list.clear()
print(i)
#将背景模型转换为图像,然后将其转换为RGB格式并保存到指定路径
bg=Image.fromarray(background)
bg.convert('RGB').save('D:\\cg\\centerW.jpg')
return background
#设置一个循环,不断读取视频帧并将其转换为灰度图像。
# frameNum记录当前帧的编号。
# 对于每帧,计算其分辨率,并在frameNum大于等于m+1时调用creatbackground函数进行背景建模。
frameNum=0
while(capture.isOpened()):
ret,frame=capture.read()
frame=cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
frameNum+=1
row=frame.shape[0]
col=frame.shape[1]
if frameNum>=m+1:
background=creatbackground(row,col,frameNum,m)
cap.release()
cv2.destroyAllWindows()
②均值法
在一段时间内取连续N帧视频图像序列,对于每个像素点计算这N帧图像在此点处N个像素值的平均值,并将此值作为该点背景图像中的灰度值。该方法效率高,适用于静态且光照恒定的场景。代码与中值法类似。
优点:
- 计算简单:计算简单,易于实现。
- 平滑效果好:能够有效地去除噪声,平滑图像。
缺点:
- 敏感噪声:对噪声敏感,容易受到噪声的影响。
- 光照变化敏感:对光照变化敏感,容易受到光照变化的影响。
③高斯模型法
高斯模型法认为,背景像素值的分布服从高斯分布,即:
其中:
x
是像素点的灰度值μ
是背景像素值的均值σ^2
是背景像素值的方差,
对于当前像素点,如果其灰度值与周围像素点的灰度值差异较大,则认为该像素点不属于背景。具体来说,高斯模型法使用以下公式来计算当前像素点与周围像素点的灰度值差异:
其中:
D(x)
是当前像素点与周围像素点的灰度值差异x_i
是周围像素点的灰度值n
是周围像素点的个数
如果 D(x)
大于阈值 T
,则认为该像素点不属于背景
优点:
- 鲁棒性强:能够有效地去除噪声和光照变化的影响
- 准确性高:能够准确地提取目标的边缘
缺点:
- 计算量大:计算量较大,尤其是对于大图像而言
- 对参数敏感:性能对参数
μ
和σ^2
敏感
二.光流法
光流法(Optical Flow)基于这样一个假设:随着时间的推移,相邻图像帧中的物体或场景的运动可以通过观察每个像素点的移动来检测和描述。换句话说,光流是图像中物体运动的视觉表现,它表达了图像中物体或场景点在连续两帧之间的位移向量。
1.基本假设
光流法是一种从图像序列中估计运动信息的方法。它基于以下假设:
- 相邻图像之间的时间间隔很小,场景中的物体运动缓慢。
- 同一目标在不同帧间进行运动时,其亮度不会发生变化
根据上述假设,光流法可以将图像序列中的每个像素点与其在相邻图像中的对应点进行匹配,并计算出对应点的运动速度。
2.基本约束方程
光流方程可以表示为:
其中,和是图像在空间坐标上的导数,是图像在时间上的导数,和分别是图像点在和方向上的运动速度,即光流矢量的两个分量。
3.光流矢量计算
光流方法可以分为两类:稠密光流和稀疏光流。
-
稠密光流:为图像中的每一个像素计算光流向量。这种方法可以提供详尽的运动信息,但计算成本较高。代表性算法有Gunnar Farneback算法。
-
稀疏光流:只在图像中的某些特征点上计算光流向量,这些特征点通常是图像中的角点或者其他容易追踪的点。这种方法计算成本较低,但只能提供有限的运动信息。代表性算法有Lucas-Kanade方法。
本处介绍使用Horn-Schunck方法来计算。Horn-Schunck方法是一种稠密光流估计方法,它假设整个图像的光流场是平滑的,即图像中每个像素都有光流矢量,并且相邻像素之间的光流矢量变化是平滑的。
该方法通过最小化一个能量函数来求解光流矢量,能量函数由两部分组成:数据项(基于亮度恒定假设)和平滑项(假设光流场平滑)。能量函数如下:
其中,α是一个平滑参数,控制数据项和平滑项之间的权衡。较小的α值允许更多的局部变化,而较大的α值则强制整个光流场更加平滑。
为了最小化能量函数,Horn-Schunck方法使用变分法来求解偏微分方程(PDE)。通过对能量函数进行变分,得到关于u和v的Euler-Lagrange方程,然后使用迭代方法求解这些方程。
具体来说,求解过程包括初始化光流矢量u,v为零(或其他初始估计),然后迭代地更新�u和�v以最小化能量函数。每次迭代中,u和v的更新公式可以通过离散化的偏微分方程得到,通常采用高斯-赛德尔迭代或雅可比迭代方法。
三.Vibe算法(Visual Background Extractor)
1.背景模型初始化
需要一定长度的视频序列进行学习才可以完成。背景模型初始化只需要对每个像素点进行一次随机采样,计算量较小。ViBe算法中背景模型初始化是随机采样的,且背景模型初始化只使用一帧图像。
具体步骤如下:
- 对于第一帧图像中的每个像素点,随机选择其周围 N 个像素点,并将这些像素点的灰度值作为当前像素点的背景模型。
- 对每个像素点的背景模型进行排序,并取中间的
M
个像素点的灰度值作为该像素点的最终背景模型。
2.前景检测
对上一步所得到的背景模型作前景检测。
具体步骤:计算当前时刻帧t中,每个像素点的像素值与背景模型中相应像素点的所有样本的偏差。若偏差小于相似度阈值R,则认为该像素点与背景模型中相应像素点的相似度较高。统计背景模型中与当前像素点相似度较高的样本点数,当此类相似样本点数超过σ时,则认为该帧中,此像素点为背景点B,否则就是前景F
- 相似度阈值 R: 控制像素点与背景模型匹配的严格程度。
R
越小,匹配越严格,需要更小的偏差才能被认为是相似。 - 相似样本点数阈值
σ
: 控制像素点被认为是背景所需的相似样本数量。σ
越大,需要越多的相似样本才能被认为是背景。
3.背景模型更新
此算法包含三个子策略:保守更新策略、前景点计数策略和随机子采样策略。
保守更新策略指前景点永远不会用于填充背景模型;前景点计数策略指对像素点进行统计时,如果某个点连续N次都被记为背景点,则将其更新为背景点;随即子采样策略指不需要对所有新视频帧都去更新背景模型中的每一个像素点的样本值。
4.代码
import numpy as np
import random
import cv2
from PIL import Image
#定义Vibe算法类
class Vibe:
_defaultNbSamples=20 #每个像素点的样本数
_defaultReqMatches=2 #为了认定一个像素属于背景,它在样本集中需要有至少2个样本与之匹配
_defaultRadius=20 #判断像素值是否匹配的距离阈值
_defaultSubsamplingFactor=16 #子采样概率,用于控制样本更新的频率,默认为16
_BG=0 #背景像素值
_FG=255 #前景像素值
#分别存储9个邻近点相对于当前点的x和y偏移量,用于随机选择样本更新或初始化时的邻近像素
_c_xoff=[-1,0,1,-1,1,-1,0,1,0] #x的邻居点len=9
_c_yoff=[-1,0,1,-1,1,-1,0,1,0] #y的邻居点len=9
_samples=[] #保存每个像素点的样本值
_Height=0
_Width=0
def __init__(self,grayFrame):
#初始化帧的高度和宽度属性,然后为每个像素创建一个样本集
self._Height=grayFrame.shape[0]
self._Width=grayFrame.shape[1]
for i in range(self._defaultNbSamples+1):
self._samples.insert(i,np.zeros(grayFrame.shape[0],grayFrame.shape[1]),dtype=grayFrame.dtype)
self.__init_params(grayFrame)
def __init_params(self,grayFrame):
# 记录随机生成的行r 列c
rand=0
r=0
c=0
#对每个像素样本进行初始化
for y in range(self._Height):
for x in range(self._Width):
#循环每一个像素的样本集
for k in range(self._defaultNbSamples):
#随机获取样本像素值
rand=random.randint(0,8)
r=y+self._c_yoff[rand]
if r<0:
r=0
if r>self._Height:
r=self._Height-1
c=x+self._c_xoff[rand]
if c<0:
c=0
if c>self._Width:
c=self._Width-1
#存储像素样本值,即从grayFrame随机选取了临近的像素点值赋给sample数组
self._samples[k][y,x]=grayFrame[r,c]
self._samples[self._defaultNbSamples][y,x]=0
def update(self,grayFrame):
foreground=np.zeros((self._Height,self._Width),dtype=np.uint8)
for y in range(self._Height):
for x in range(self._Width):
count=0 #统计当前像素点在样本集中匹配的样本数量
index=0 #指示当前正在检查的样本索引,直到检查完所有样本或找到足够的匹配样本
dist=0.0 #计算当前像素点与样本之间的距离
while(count<self._defaultReqMatches)and(index<self._defaultNbSamples):
dist=float(grayFrame[y,x])-float(self._samples[index][y,x])
if dist<0:
dist=-dist
if dist<self._defaultRadius:
count+=1 #此时则认为该样本与当前像素点匹配
if count>=self._defaultReqMatches:
#判断为背景点,只有背景点才能用于传播和更新样本值
self._samples[self._defaultNbSamples][y,x]=0
foreground[y,x]=self._BG
rand=random.randint(0,self._defaultSubsamplingFactor)#十六分之一的更新概率
if rand==0:
rand=random.randint(0,self._defaultNbSamples)
#随机更新20样本中的一个
self._samples[rand][y,x]=grayFrame[y,x]
rand=random.randint(0,self._defaultSubsamplingFactor)
if rand==0:
#获取随机像素 填充20个样本中的任意一个
rand=random.randint(0,8)
yN=y+self._c_yoff[rand]
if yN<0:
yN=0
if yN>=self._Height:
yN=self._Height-1
rand=random.randint(0,8)
xN=x+self._c_xoff[rand]
if xN<0:
xN=0
if xN>=self._Width:
xN=self._Width-1
rand = random.randint(0, self._defaultNbSamples)
self._samples[rand][yN,xN]=grayFrame[y,x]
else:
#判断为前景
foreground[y,x]=self._FG
self._samples[self._defaultNbSamples][y,x]+=1
if self._samples[self._defaultNbSamples][y,x]>50:
rand=random.randint(0,self._defaultNbSamples)
self._samples[rand][y,x]=grayFrame[y,x]
return foreground
#导入需要处理的视频
capture=cv2.VideoCapture('D:\\素材\\1.mp4')
ret,frame=capture.read()
frame=cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
#遍历视频序列中的n视频帧,n取500
for i in range(500):
vb=Vibe(frame)
ret,frame=capture.read()
frame1=cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
foreground=vb.update(frame1)#生成前景与背景
im=Image.fromarray(foreground)#将数值矩阵转换为图片格式
#im.show()
frame=frame1#更新用于构建背景模型的图像