【【目标跟踪实战】运动目标检测、背景建模(帧差法,单高斯、混合高斯)opencv实战-哔哩哔哩】 https://b23.tv/j2VSkUe
原理:
高斯混合模型,也被称为混合高斯模型,被广泛的用于鲁棒的复杂场景背景建模,特别是在有微小重复运动的场合。基于像素的高斯混合模型对多峰分布的背景进行建模很有效,能适应背景的变化,并能基本满足实际应用中对算法的实时性要求。
基本思想:对每一个像素点定义多个状态来表示其所呈现的颜色
假设某个像素近t帧的特征值为,若用高斯混合模型对该点建模,那么当前像素的像素值的概率可以表示为:
:表示某个像素在t时刻的取值,若像素为RGB三通道 ,则
:高斯分布的数目,取值根据实际情况一般为3~5,
越大代表处理波动的能力越强,相应所需的处理时间也就越长
:时刻t时第i个高斯分布的权重,迭代更新参数
:时刻t时第i个高斯分布的均值,迭代更新参数
:时刻t时第i个高斯分布的协方差,迭代更新参数
:高斯概率分布表示为
建模方法:
基本思想:
把每一个像素点所呈现的颜色用K个高斯分布叠加来表示,通常K取3~5之间。将像素点所呈现的颜色X认为是随机变量,则在每时刻t=1,...,T所得到视频帧图像的像素值只是随机变量X的采样值(观值)
在进行背景建模前,先对背景进行训练,对每一帧图像中每个背景采用一个混合高斯模型进行模拟,背景一旦提取出来,前景的检测就简单了,检查像素是否与背景的高斯模型匹配,匹配则为背景,不匹配则为前景。由于整个过程GMM都在不断更新学习的过程,所以对于动态背景该算法也具有一定的鲁棒性。
GMM能区分前景后景的原因:
①在长期观测场景中,背景占大多数时间,更多的数据是支持背景分布的
②即使是相对颜色一致的运动物体也会比背景产生更多的变化,况且一般情况下物体都是带有不同颜色的
建模过程:
①参数初始化:
认为第一帧视频图像为背景的可能性较大,即使第一帧图像有些区域为运动目标,相对于整幅图像来说,运动区域也只占比较小的一部分,取第一帧图像的像素值来对混合高斯模型中某个高斯分布的均值进行初始化,并对该高斯分布的权值取相对最大值,其他高斯分布的均值取“0”,权重相等,混合高斯模型中所有高斯函数的方差取相等的较大初始化值,那么在混合高斯参数的学习过程中,取该较大权重的高斯函数均值作为场景背景的可能性较大
在第一帧图像时每个像素对应的第一个高斯分布进行初始化,均值赋给当前像素的值,权值赋为“1”,除第一以外的高斯分布函数的均值、权值和都初始化为“0”
②参数更新:
在时刻t对图像帧的每个像素与它对应的高斯模型进行匹配
匹配规则:
在获得第t时刻的像素值以后,若新获取的像素值与第k个高斯分布满足下式,则认为该像素值与该高斯分布匹配:
:用户自定义参数,实际中取值一般为2.5
(1)对于不匹配的高斯分布,它们的均值和标准差保持不变
(2)匹配的高斯分布的均值
和标准差按下式更新:
:学习率
若该像素对应的混合高斯模型中没有高斯分布与像素值 匹配,那么将最不可能代表背景过程的高斯分布
重新赋值,即该高斯分布的均值为当前像素值,标准差为初始较大值,权重为较小值
各模型的权重按如下公式更新:
:学习率
:表示第t帧中第k个分布是否匹配,一般情况下,匹配时,
,反之,
;之后需要对更新后的权重重新进行归一化,保证权重和为“1”
③背景模型估计:
图像帧中每个像素的混合高斯模型的参数更新后,要确定混合高斯模型中哪些高斯分布是由背景过程产生的,对于K个分布,必须估计出其中哪些代表背景模型。
在实际应用中,处理完当前帧后,模型的参数要进行必要的更新,所以每一帧都要进行背景模型的重新估计
(1)当背景物体是持久静止时,此时该物体表面产生的高斯分布代表着背景分布,那么支持这个分布的数据就会累加,而且它的方差会越来越小
(2)当一个新的物体遮挡了原来的背景物体时,一般会导致两种结果:要么产生一个新的分布,要么把一个已存在的分布的方差增大。另外,当新物体是一个运动物体时,它一般也会比背景像素保持更大的变化直到它停下来
考虑因素:
该分布产生的数据所占的比例大小
该分布的方差大小
估计方法:
按从大到小排列将每个像素混合高斯模型的K个高斯分布排序,
越大,排的顺序越靠前,否则排的顺序靠后。分布位置越靠前,它是背景分布的可能性就越大,否则它是背景分布的可能性就越小,排在最后的高斯分布将会被新建立的分布所取代。
若是各高斯分布在t时刻按
由大到小的排列次序,若前
个分布满足下式:
则这个分布被认为是背景分布,其余高斯分布被认为是运动前景分布,其中参数
表示背景所占的比例,通常取0.25或0.75
④运动前景检测:
从新检验t时刻每一个像素值与其得到的前
个高斯分布的匹配关系,若当前像素值
和每个背景高斯分布均值之差的绝对值大于该分布标准差的
倍,则
被认为是运动前景,否则
被认为是背景像素。
像素只要与一个背景高斯分布匹配,就判定为背景像素,即满足下式的像素被检测为目标:
举例说明:
想象这个过程像是在“训练一只智能小猫”,它会看图片,然后学会“这是什么样的背景”。
1. 背景是什么?
• 如果你让小猫连续看几秒钟的监控视频,它会发现:
• 大部分时候,草地是绿色的,天空是蓝的,这些就是“背景”。
• 偶尔画面里出现了一辆车或者一个人走过去,这些是“前景”。
• 小猫会用数学的方法,把这些像素点的颜色归纳为几个“典型模式”(对应高斯分布),比如:
• 绿色草地是一个模式(分布),蓝色天空是另一个。
• 运动的物体(比如车或人)可能会生成新的模式,但这些模式很快消失。
2. 怎么区分“背景”和“前景”?
• 小猫每次看到一个新的像素点(比如颜色值),它会问自己:
“这个颜色值和我记住的背景模式像不像?”
• 像:那应该是背景。
• 不像:哦,有点奇怪,可能是前景。
• 判断“像不像”的标准是用一个数学公式:
• 意思是:只要像素点跟某个高斯分布的均值
的差距,不超过这个分布的范围(协方差
决定范围大小),就算“像”。
3. 背景会变,怎么办?
• 如果有风吹过,草地的绿色稍微变了,或者天色暗了下来,没问题!
• 小猫会把新的像素值逐渐加入“记忆”:
• 颜色变化小:更新已有的背景分布。
• 颜色变化大:创建新的分布,或者替换不常用的分布。
• 随着时间推移,小猫的背景建模会越来越适应环境。
用场景解释一下
假设有一段监控视频,只有草地和天色:
1. 初始化(小猫第一次学习):
• 草地是绿色,高斯分布
• 天空是蓝色,高斯分布
2. 新的一帧图片来了:
• 如果像素点的颜色很接近草地或天空的分布,说明它是背景。
• 如果颜色突然很奇怪(比如红色的衣服),小猫就会判断:
“诶,这不是背景!是前景!”
3. 适应性:
• 一只鸟飞过,画面中有了新的像素值(比如灰色),小猫可能会短暂记住“灰色鸟”这个分布,但过一会儿它飞走了,小猫会忘记这部分记忆。
实验:
import cv2
# 指定视频文件路径
filepath = 'D:\\pycharm2024\\PycharmProjects\\learn_pytorch\\opencv\\gaus.mp4'
video = cv2.VideoCapture(filepath)
# 创建混合高斯模型用于背景建模
back = cv2.createBackgroundSubtractorMOG2()
# 自定义卷积核,用于形态学处理
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
while True:
ret, frame = video.read()
if not ret:
break
# 应用背景建模
img = back.apply(frame)
# 开运算(先腐蚀后膨胀),去除噪声
img_close = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
# 轮廓检测
contours, hierarchy = cv2.findContours(img_close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
length = cv2.arcLength(cnt, True)
if length > 188:
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
# 显示结果
cv2.imshow('frame', frame)
cv2.imshow('img', img)
# 设置退出条件
if cv2.waitKey(100) & 0xFF == 27:
break
video.release()
cv2.destroyAllWindows()
import multiprocessing as mp
import cv2
import numpy as np
# alpha 就是论文中的更新速率 learning rate,alpha = 1 / default_history
# default_history 表示训练得到背景模型所用到的集合大小,默认为 500,并且如果不手动设置 learning rate 的话,这个变量 default_history 就被用于计算当前的
# learning rate,此时 default_history 越大,learning rate 就越小,背景更新也就越慢
default_history = 200
# 方差阈值,用于判断当前像素是前景还是背景
default_var_threshold = 4.0 * 4.0
# 每个像素点高斯模型的最大个数,默认为 5
default_nmixtures = 5
default_background_ratio = 0.9
# 方差阈值,用于是否存在匹配的模型,如果不存在则新建一个
default_var_threshold_gen = 3.0 * 3.0
# 初始化一个高斯模型时,方差的值默认为 15
default_var_init = 15.0
# 高斯模型中方差的最大值为 5 * 15 = 75
default_var_max = 5 * default_var_init
# 高斯模型中方差的最小值为 4
default_var_min = 4.0
default_ct = 0.05
CV_CN_MAX = 512
FLT_EPSILON = 1.19209e-07
class GuassInvoker():
def __init__(self, image, mask, gmm_model, mean_model, gauss_modes, nmixtures, lr, Tb, TB, Tg, var_init, var_min,
var_max, prune, ct, nchannels):
self.image = image
self.mask = mask
self.gmm_model = gmm_model
self.mean_model = mean_model
self.gauss_modes = gauss_modes
self.nmixtures = nmixtures
self.lr = lr
self.Tb = Tb
self.TB = TB
self.Tg = Tg
self.var_init = var_init
self.var_min = var_min
self.var_max = var_max
self.prune = prune
self.ct = ct
self.nchannels = nchannels
# 针对原图像中的某一行进行处理,row 是图像中某一行的下标
def calculate(self, row):
lr = self.lr
gmm_model = self.gmm_model
mean_model = self.mean_model
# image 中的某一行像素
data = self.image[row]
cols = data.shape[0]
# 遍历原图像中的某一行的所有列
for col in range(cols):
background = False
fits = False
# 当前像素点的高斯模型个数
modes_used = self.gauss_modes[row][col]
total_weight = 0.
# 当前像素点使用的所有高斯模型
gmm_per_pixel = gmm_model[row][col]
# 当前像素点使用的所有高斯模型的均值
mean_per_pixel = mean_model[row][col]
costs = []
flag = False
for mode in range(modes_used):
# 一个 gmm 结构体的结构为: [weight, variance, c],所以只有 c 值大于 0,才会进行计算 abs(x - mean) / variance
# c 值表示和对应高斯模型匹配的像素点的个数
if gmm_per_pixel[mode][2] > 0:
_cost = np.sum((np.array(data[col]) - np.array(mean_per_pixel[mode])) ** 2)
_cost = np.sqrt(_cost / float(gmm_per_pixel[mode][1]))
# 将 [mode, abs(x - mean) / variance] 的值保存到 costs 中,其中 mode 是高斯模型的索引
costs.append(np.array([mode, _cost]))
flag = True
min_cost = 4.0 * 4.0
if flag:
_, min_cost_index = np.argmin(np.array(costs), axis=0)
min_index, min_cost = costs[min_cost_index]
# 只有当前 abs(x - mean) / variance < Tg 时,才会认为像素点的值符合当前高斯模型
if flag and min_cost < self.Tg:
fits = True
# 计算当前像素点所有高斯模型中 c 值之和,c 值也就是每一个高斯模型匹配上的像素点的个数,加一是因为现在有匹配上了一个像素点
sum_c = np.sum(gmm_per_pixel[:, 2]) + 1
# 计算权重 weight 的更新速率值,在之前的算法中,权重 weight,方差 variance 以及均值 mean 的更新速率全部为一个相同的固定值,
# 而改进的算法中,weight, variance, mean 都是由不同速率进行更新,加快收敛速度
lr_w = max(1.0 / sum_c, self.lr)
# 遍历每一个高斯模型
for mode in range(modes_used):
# gmm 高斯模型是一个 3 维向量,组成为 [weight, variance, c]
gmm = gmm_per_pixel[mode]
# 当前高斯模型匹配上的像素点的个数
c_mode = gmm[2]
# 对所有高斯模型的 weight 权重值进行更新,更新的公式为:weight = (1 - lr_w) * weight - lr_w * ct + lr_w * q
# 其中 q 只有对于当前最符合的高斯模型,值才为 1,其余的都为 0
weight = (1 - lr_w) * gmm[0] - lr_w * self.ct
mean = mean_per_pixel[mode]
var = gmm[1]
d_data = data[col] - mean
dist2 = np.sum(d_data ** 2)
# 使用马氏距离来判断当前像素点是属于前景还是背景
if total_weight < self.TB and dist2 < self.Tb * var:
background = True
swap_count = 0
# 如果当前高斯模型时最符合的
if mode == min_index:
gmm[2] += 1
# 当前高斯模型是最符合的,因此 q = 1,因此 weight 需要加上 lr_w * q = lr_w
weight += lr_w
# lr_mean 时均值 mean 的更新速率, lr_var 是方差 variance 的更新速率
lr_mean = max(1.0 / c_mode, self.lr)
lr_var = self.lr
if c_mode > 1:
lr_var = max(1.0 / (c_mode - 1), self.lr)
# 分别使用 lr_mean 和 lr_var 更新 mean 和 variance
mean = mean + lr_mean * d_data
mean_per_pixel[mode] = mean
var = var + lr_var * (dist2 - var)
var = max(var, self.var_min)
var = min(var, self.var_max)
gmm[1] = var
for i in range(mode, 0, -1):
if weight < gmm_per_pixel[i - 1][0]:
break
swap_count += 1
gmm_per_pixel[i - 1], gmm_per_pixel[i] = gmm_per_pixel[i], gmm_per_pixel[i - 1]
mean_per_pixel[i - 1], mean_per_pixel[i] = mean_per_pixel[i], mean_per_pixel[i - 1]
# 保证下一次模型的权重非负,lr_w * ct,也 就是 weight 要大于 lr_w * ct,否则当前高斯模型的 weight 可能就会出现小于 0 的情况。
# 如果 weight 小于常量值,那么必须将当前像素点的 nmodes 减一,也就是舍弃掉当前高斯模型,实现论文里面所说的模型个数自适应变化
if weight < self.ct * lr_w:
weight = 0
modes_used -= 1
gmm_per_pixel[mode - swap_count][0] = weight
total_weight += weight
if not fits and lr > 0:
# 如果模型数已经达到最大 nmixtures 时,替换掉权值最小的那个高斯模型; 否则,新增一个高斯模型
if modes_used == self.nmixtures:
mode = self.nmixtures - 1
else:
mode = modes_used
modes_used += 1
# 如果只有一个高斯模型,那么就把这个高斯模型的权重设置为 1
if modes_used == 1:
gmm_per_pixel[mode][0] = 1.0
else:
# 新增加模型的权重等于 lr,也就是 learning rate
# 当前像素点有 nmixtures 个高斯模型,并且这些高斯模型是按照权重大小降序排列的
gmm_per_pixel[mode][0] = lr
for i in range(mode):
gmm_per_pixel[i][0] *= (1 - lr)
# 初始化新的高斯模型的均值 mean,使用的就是原始图像中的像素点的值来进行初始化
mean_per_pixel[mode] = data[col]
# 初始化新增的混合高斯模型 gmm 的方差 variance
gmm_per_pixel[mode][1] = self.var_init
# 初始化新增高斯模型的 c 值为 1
gmm_per_pixel[mode][2] = 1
# 对所有的高斯模型按照权重进行降序排序
for i in range(modes_used - 1, 0, -1):
if lr < gmm_per_pixel[i - 1][0]:
break
gmm_per_pixel[i - 1], gmm_per_pixel[i] = gmm_per_pixel[i], gmm_per_pixel[i - 1]
mean_per_pixel[i - 1], mean_per_pixel[i] = mean_per_pixel[i], mean_per_pixel[i - 1]
self.gauss_modes[row][col] = modes_used
self.mask[row][col] = 0 if background else 255
# 对 gmm_per_pixel 中的各个高斯模型的权重值进行归一化
total_weight = np.sum(gmm_per_pixel[:, 0])
inv_factor = 0.
if abs(total_weight) > FLT_EPSILON:
inv_factor = 1.0 / total_weight
gmm_per_pixel[:, 0] *= inv_factor
return row, self.mask[row], self.gmm_model[row], self.mean_model[row], self.gauss_modes[row]
# noinspection PyAttributeOutsideInit
class GuassMixBackgroundSubtractor():
def __init__(self):
self.frame_count = 0
self.history = default_history
self.var_threshold = default_var_threshold
self.nmixtures = default_nmixtures
self.var_init = default_var_init
self.var_max = default_var_max
self.var_min = default_var_min
self.var_threshold_gen = default_var_threshold_gen
self.ct = default_ct
self.background_ratio = default_background_ratio
def apply(self, image, lr=-1):
if self.frame_count == 0 or lr >= 1:
self.initialize(image)
self.image = image
self.frame_count += 1
# 计算 learning rate,也就是 lr,有以下三种情况:
# 1.输入 lr 为 -1,那么 lr 就按照 history 的值来计算
# 2.输入 lr 为 0,那么 lr 就按照 0 来计算,也就是说背景模型停止更新
# 3.输入 lr 在 0 ~ 1 之间,那么背景模型更新速度为 lr,lr 越大更新越快,算法内部表现为当前帧参与背景更新的权重越大
self.lr = lr if lr >= 0 and self.frame_count > 1 else 1 / min(2 * self.frame_count, self.history)
pool = mp.Pool(int(mp.cpu_count()))
self.mask = np.zeros(image.shape[:2], dtype=int)
# 对原图像中的每一行进行并行计算
result = pool.map_async(self.parallel, [i for i in range(self.image.shape[0])]).get()
pool.close()
pool.join()
# 计算完成之后再进行组合,得到最后的结果
for row, mask_row, gmm_model_row, mean_model_row, gauss_modes_row in result:
self.mask[row] = mask_row
self.gauss_modes[row] = gauss_modes_row
self.mean_model[row] = mean_model_row
self.gmm_model[row] = gmm_model_row
return self.mask
def parallel(self, row):
invoker = GuassInvoker(self.image, self.mask, self.gmm_model, self.mean_model, self.gauss_modes, self.nmixtures,
self.lr,
self.var_threshold, self.background_ratio, self.var_threshold_gen, self.var_init,
self.var_min, self.var_max, float(-self.lr * self.ct), self.ct, self.nchannels)
return invoker.calculate(row)
def initialize(self, image):
# gauss_modes 这个矩阵用来存储每一个像素点使用的高斯模型的个数,初始的时候都为 0
self.gauss_modes = np.zeros(image.shape[:2], dtype=int)
height, width = image.shape[:2]
if len(image.shape) == 2:
self.nchannels = 1
else:
self.nchannels = image.shape[2]
# 高斯混合背景模型分为两部分:
# 第一部分:height * width * nmixtures (=5) * 3 * sizeof(float),
# 3 表示包含 weight, variance 以及 c 三个 float 变量,也就是 gmm_model,其中 c 表示和这个高斯模型匹配的个数
# 第二部分:height * width * nmixtures (=5) * nchannels * sizeof(float),
# nchannels 一般为 3,表示 B, G, R 三个变量,其实也就是 mean 每个像素通道均对应一个均值,
# 刚好有 nchannels 个单位的 float 大小,也就是 mean_model
# nmixtures = 5 表示高斯模型的数量最多为 5 个
self.gmm_model = np.zeros((height, width, self.nmixtures, 3), dtype=float)
self.mean_model = np.zeros((height, width, self.nmixtures, self.nchannels), dtype=float)
def get_background(self, frame):
height, width = self.gauss_modes.shape[:2]
background = np.zeros((height, width, self.nchannels), dtype=np.uint8)
for row in range(height):
for col in range(width):
max_weight = -1
bg_mean = None
for mode in range(self.gauss_modes[row, col]):
gmm = self.gmm_model[row, col, mode]
mean = self.mean_model[row, col, mode]
weight = gmm[0]
var = gmm[1]
# Check if this Gaussian model satisfies the background criteria
if weight / (1 - (1 - self.lr) ** gmm[2]) > self.background_ratio:
if weight > max_weight:
max_weight = weight
bg_mean = mean
if bg_mean is not None:
background[row, col] = bg_mean
return background
if __name__ == '__main__':
img = cv2.imread('blank.png')
cap = cv2.VideoCapture("D:\\pycharm2024\\PycharmProjects\\learn_pytorch\\opencv\\768x576.avi")
if not cap.isOpened():
# 如果没有检测到摄像头,报错
raise Exception('Check if the camera is on.')
# Get video properties
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = 12.0
fourcc = cv2.VideoWriter_fourcc('M', 'P', '4', '2')
# Initialize video writers for foreground and background
foreground_writer = cv2.VideoWriter('/home/macy/Data/imageclass/foreground_output.avi', fourcc, fps,
(width, height))
background_writer = cv2.VideoWriter('/home/macy/Data/imageclass/background_output.avi', fourcc, fps,
(width, height))
mog = GuassMixBackgroundSubtractor()
frame_count = 0
while cap.isOpened():
catch, frame = cap.read()
frame_count += 1
if not catch:
print('The end of the video.')
else:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
mask = mog.apply(gray).astype('uint8')
mask = cv2.medianBlur(mask, 3)
cv2.imwrite('/home/macy/Data/imageclass/mask' + str(frame_count) + '.jpg', mask)
print('writing mask' + str(frame_count) + '.jpg...')
# Extract foreground using the mask
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
if cv2.contourArea(contour) > 200:
# Get bounding box for each contour and draw it on the original frame
x, y, w, h = cv2.boundingRect(contour)
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
# Extract background using the inverse of the mask
background = mog.get_background(frame)
# Save original frame, foreground, and background
cv2.imwrite('/home/macy/Data/imageclass/original' + str(frame_count) + '.jpg', frame)
cv2.imwrite('/home/macy/Data/imageclass/background' + str(frame_count) + '.jpg', background)
print(f'Processed frame {frame_count}...')
foreground_writer.write(mask)
background_writer.write(background)
cap.release()
foreground_writer.release()
background_writer.release()