计算机视觉进阶教学之背景建模与光流估计
目录
简介
一、背景建模
1.帧差法的原理
2.基于K近邻的背景
3.基于高斯混合的背景
4.代码实现
二、光流估计
1.代码实现
简介
在计算机视觉的应用场景中,“静态图像分析”(如图像分类、目标检测)只是基础,更具挑战性的需求往往藏在动态视频场景中:交通监控中如何实时区分车辆与道路背景?无人机航拍如何追踪移动的行人?自动驾驶如何感知周边物体的运动轨迹?
这些问题的解决,离不开两大核心技术 ——背景建模与光流估计。前者负责从动态视频中 “剥离” 稳定的背景与变化的前景,后者则专注于分析运动物体的 “速度与方向”,二者共同构成了动态场景理解的技术基石。
一、背景建模
背景建模是什么?
指在计算机视觉中,从视频序列中提取出静态背景的一种技术。在视频中,背景通常被定义为相对稳定的部分,例如墙壁、地面或天空等。背景建模的目标是将动态的前景对象与静态的背景进行分离,以便进一步分析和处理。
背景建模的目的?
通过背景建模,我们可以实现很多应用,例如运动检测、目标跟踪、
三种方法基本原理都是差不多的
1.帧差法的原理
由于场景中的目标在运动,目标的影像在不同图像帧中的位置不同。该类算法对时间上连续的两帧图像进行差分运算,不同帧对应的像素点相减,判断灰度差的绝对值,当绝对值超过一定阈值时,即可判断为运动目标,从而实现目标的检测功能。
帧差法的优缺点
帧差法非常简单,但是会引入噪音和空洞(人物中间是黑色的)问题
2.基于K近邻的背景
- 原理:对于每个像素,维护一个长度为
K
的样本集,这些样本代表了该像素在过去一段时间内的背景值。当新的像素值到来时,计算它与样本集中K
个样本的距离,根据距离判断该像素是属于背景还是前景。如果距离足够小,说明与背景样本相似,属于背景;否则属于前景。 - 特点:能够较好地适应背景的缓慢变化,比如光照的缓慢变化等。同时,通过调整
K
值等参数,可以在检测精度和计算效率之间进行权衡。相比帧差法,对背景变化的适应性更强,但计算复杂度相对较高。
3.基于高斯混合的背景
- 原理:为每个像素建立多个高斯分布的混合模型,每个高斯分布对应像素的一种可能的背景状态(比如不同的光照、纹理等情况)。通过持续更新这些高斯分布的参数(均值、方差、权重等),来适应背景的变化。当新的像素值到来时,判断它是否符合这些高斯分布所代表的背景模型,如果不符合,则认为是前景。
- 特点:可以很好地处理背景的复杂变化,比如光照的突然变化、树叶的轻微晃动等动态背景情况。能够自动调整高斯分布的数量,对不同场景的适应性很强,是实际应用中非常常用的背景建模方法,但计算复杂度相对较高,不过在现代硬件支持下,实时性也能得到保障。
4.代码实现
import cv2
# 打开测试视频文件,'test.mp4'为视频路径,也可替换为0(代表打开默认摄像头)来捕获实时视频流
cap = cv2.VideoCapture('test.mp4')
# cap = cv2.VideoCapture(0)# getStructuringElement函数用于获取形态学操作的结构元素(卷积核)
# 参数cv2.MORPH_CROSS表示使用十字形结构元素,(3, 3)是结构元素的大小
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
# 创建基于高斯混合模型的背景减除器,用于从视频帧中分离背景和前景
fgbg = cv2.createBackgroundSubtractorMOG2()while True:# 读取视频的一帧,ret为布尔值,代表是否成功读取帧;frame为读取到的帧图像ret, frame = cap.read()if not ret: # 如果没有成功读取到帧(比如视频结束),则跳出循环break# 展示原始视频帧cv2.imshow('frame', frame)# 应用背景减除器到当前帧,得到前景掩码(fgmask),前景区域为白色,背景为黑色fgmask = fgbg.apply(frame)cv2.imshow('fgmask', fgmask)# 对前景掩码进行开运算(先腐蚀后膨胀),用于去除小的噪声点fgmask_new = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel)cv2.imshow('fgmask1', fgmask_new)# 寻找前景掩码中的外部轮廓,cv2.RETR_EXTERNAL表示只检测最外层轮廓,cv2.CHAIN_APPROX_SIMPLE表示压缩轮廓点contours, h = cv2.findContours(fgmask_new, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)for c in contours: # 遍历每个轮廓# 计算轮廓的周长,True表示轮廓是闭合的perimeter = cv2.arcLength(c, True)if perimeter > 188: # 当轮廓周长大于188时,认为是需要关注的目标# 计算轮廓的边界矩形,x、y为矩形左上角坐标,w、h为矩形的宽和高x, y, w, h = cv2.boundingRect(c)# 在原始帧上绘制这个边界矩形,颜色为(0, 255, 0)(绿色),线宽为2fgmask_new_rect = cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)# 展示绘制了边界矩形的帧cv2.imshow('fgmask_new_rect', fgmask_new_rect)# 等待60毫秒,如果按下ESC键(ASCII码为27),则跳出循环k = cv2.waitKey(60)if k == 27:break
# 释放视频捕获对象,关闭视频文件或摄像头
cap.release()
# 关闭所有OpenCV创建的窗口
cv2.destroyAllWindows()
二、光流估计
光流估计是什么?
是空间运动物体在观测成像平面上的像素运动的“瞬时速度”,根据各个像素点的速度矢量特征,可以对图像进行动态分析,例如目标跟踪。
光流估计的前提?
- (1)亮度恒定:同一点随着时间的变化,其亮度不会发生改变。
- (2)小运动:随着时间的变化不会引起位置的剧烈变化,只有小运动情况下才能用前后帧之间单位位置变化引起的灰度变化去近似灰度对位置的偏导数。
- (3)空间一致:一个场景上邻近的点投影到图像上也是邻近点,且邻近点速度一致。因为光流法基本方程约束只有一个,而要求x,y方向的速度,有两个未知变量。所以需要连立n多个方程求解。
1.代码实现
import numpy as np
import cv2# 打开视频文件,参数为视频路径,若要调用摄像头可改为cv2.VideoCapture(0)
cap = cv2.VideoCapture('test.avi')# 随机生成 100 组颜色值,用于为不同特征点的轨迹绘制不同颜色
color = np.random.randint(0, 255, (100, 3))# 读取视频的第一帧
ret, old_frame = cap.read()
# 将第一帧从 BGR 色彩空间转换为灰度色彩空间,便于后续特征点检测等操作
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)# 定义特征点检测的参数字典
feature_params = dict(maxCorners=100, # 最多检测的角点数量为 100 个qualityLevel=0.3, # 角点的质量水平,只有质量分数大于该值的角点才会被保留minDistance=7 # 角点之间的最小欧氏距离,避免检测到过于接近的角点
)# 利用 Shi-Tomasi 角点检测算法在第一帧灰度图中检测特征点
# mask 为 None 表示在整个图像中检测,**feature_params 是将字典参数解包传入
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)# 创建一个与原始帧大小相同的全零掩码图像,用于后续绘制特征点的轨迹
mask = np.zeros_like(old_frame)# 定义 Lucas-Kanade 光流法的参数字典
lk_params = dict(winSize=(15, 15), # 计算光流时的窗口大小,窗口越大,计算越稳定但速度越慢maxLevel=2 # 金字塔的最大层数,用于分层计算光流,提高精度和速度
)# 视频帧处理主循环
while True:# 读取下一帧视频ret, frame = cap.read()# 如果没有成功读取到帧(比如视频结束),则跳出循环if not ret:break# 将当前帧转换为灰度图frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)# 使用 Lucas-Kanade 光流法计算特征点的光流# 输入为前一帧灰度图、当前帧灰度图、前一帧的特征点,输出为当前帧的特征点、跟踪状态、误差p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)# 筛选出跟踪状态为 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)# 等待 30 毫秒,若按下 ESC 键(ASCII 码为 27)则跳出循环k = cv2.waitKey(30) & 0xffif k == 27:break# 更新前一帧的灰度图和特征点,为处理下一帧做准备old_gray = frame_gray.copy()p0 = good_new.reshape(-1, 1, 2)# 关闭所有 OpenCV 窗口
cv2.destroyAllWindows()
# 释放视频捕获对象
cap.release()