当固定摄像机观察场景时,背景基本保持不变。在这种情况下,我们真正感兴趣的目标是场景中的移动物体。为了提取这些前景物体,我们需要建立一个背景模型,然后将背景模型与当前帧进行比较,检测前景物体,前景提取是智能监控应用中的基本步骤。
如果我们拥有场景的背景图像(即不包含前景对象的帧)可供使用,那么通过简单的图像差异提取当前帧的前景:
cv::absdiff(backgroundImage,currentImage,foreground);
将差异足够高的像素视为前景像素。但是,大多数情况下,背景图像并不容易获得,实际上,很难保证给定图像中不存在前景对象。此外,背景场景通常会随着时间而变化,例如光照条件发生了变化或者背景中添加或移除了对象。
因此,有必要动态构建背景场景的模型,可以通过观察场景一段时间完成。我们假设,大多数情况下,背景的每个像素位置都是可见的,那么简单地计算所有观察值的平均值可能是一个很好的策略。然而,由于多种原因,这并不可行。首先,这将需要在计算背景之前存储大量图像;其次,当我们累积图像来计算平均图像时,无法完成前景提取;同时,无法确定何时以及应该累积多少图像来计算可用的背景模型;此外,前景对象图像会对平均背景的计算产生影响。
更好的策略是构建动态背景模型,这可以通过计算移动平均 (moving average
) 来实现。这是一种计算最新接收值的时间信号平均值的方法,如果 p t p_t pt 是给定时间 t t t 的像素值, μ t − 1 μ_{t-1} μt−1 是当前平均值,则使用以下公式更新该平均值:
u t = ( 1 − α ) μ t − 1 + α p t u_t=(1-\alpha)\mu_{t-1}+\alpha p_t ut=(1−α)μt−1+αpt
α α α 表示学习率,它定义了当前值对当前估计平均值的影响。该值越大,运行平均值适应观测值变化的速度就越快。要构建背景模型,只需计算传入帧的每个像素的运行平均值。判断像素是否为前景像素,取决于当前图像和背景模型之间的差异。
(1) 构建类 BGFGSegmentor
,使用移动平均值学习背景模型,并通过减法提取前景对象。所需的属性如下:
class BGFGSegmentor : public FrameProcessor {
cv::Mat gray; // 灰度图像
cv::Mat background; // 累积背景
cv::Mat backImage; // 当前背景图像
cv::Mat foreground; // 前景图像
double learningRate; // 学习率
int threshold; // 阈值
(2) 当前帧与背景模型进行比较,然后更新模型:
public:
BGFGSegmentor() : threshold(10), learningRate(0.01) {}
// 设置阈值
void setThreshold(int t) {
threshold= t;
}
// 设置学习率
void setLearningRate(double r) {
learningRate= r;
}
// processing method
void process(cv:: Mat &frame, cv:: Mat &output) {
// 转换为灰度图像
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
// 初始化背景图像
if (background.empty())
gray.convertTo(background, CV_32F);
background.convertTo(backImage,CV_8U);
// 计算当前图像与背景图像间的差异
cv::absdiff(backImage,gray,foreground);
// 对前景图像应用阈值
cv::threshold(foreground,output,threshold,255,cv::THRESH_BINARY_INV);
// 累积背景
cv::accumulateWeighted(gray, background,
// alpha*gray + (1-alpha)*background
learningRate, // alpha
output); // mask
}
(3) 使用视频处理框架,构建前景提取程序:
int main () {
// 创建视频处理实例
VideoProcessor processor;
BGFGSegmentor segmentor;
segmentor.setThreshold(25);
// 打开视频文件
processor.setInput("example.avi");
processor.setFrameProcessor(&segmentor);
// 显示视频
processor.displayOutput("Extracted Foreground");
processor.setDelay(1000./processor.getFrameRate());
processor.run();
cv::waitKey();
}
显示二值前景图像如下:
通过 cv::accumulateWeighted
函数可以计算图像的运移动平均值,该函数将移动平均值公式应用于图像的每个像素。生成的图像必须是浮点图像,因此我们必须在将背景模型与当前帧进行比较之前将其转换为背景图。可以通过使用简单的阈值绝对差(在 cv::absdiff
后使用 cv::threshold
计算)提取前景图像。随后使用前景图像作为 cv::accumulateWeighted
的掩码,以避免更新前景像素,因为前景图像在前景像素处被定义为 false
(即 0
),因此前景对象在结果图像中显示为黑色像素。
最后,为简单起见,我们程序构建的背景模型是基于提取的灰度图像帧,计算彩色背景需要在色彩空间中计算移动平均值,主要困难在于确定合适的阈值,以得到更加优秀结果。
上述提取场景中前景物体的方法对于背景相对稳定的简单场景效果很好。然而,在许多情况下,背景场景可能会在某些区域发生波动,从而导致频繁的假前景检测,例如,移动的背景物体(例如风中摇晃的树叶)或炫光效应(例如在水面上反射的阳光)。阴影也会带来一系列问题,因为这些阴影通常被检测为移动对象的一部分。为了解决这些问题,需要引入了更复杂的背景建模方法,例如混合高斯方法。
混合高斯方法是在移动平均值的基础上进行改进的算法。首先,该方法为每个像素维护多个移动平均值模型。这样,如果背景像素在两个值之间波动,那么就会存储两个移动平均值。只有当一个新的像素值不属于任何常观察到的模型时,它才会被声明为前景。可以通过使用参数确定模型数量,常用模型数量为 5
。
其次,不仅要为每个模型维护移动平均值,还要维护维护方差:
σ t 2 = ( 1 − α ) σ t − 1 2 + α ( p t − μ t ) 2 \sigma_t^2=(1-\alpha)\sigma_{t-1}^2+\alpha(p_t-\mu_t)^2 σt2=(1−α)σt−12+α(pt−μt)2
使用计算出的平均值和方差构建高斯模型,可以估计给定像素值属于背景的概率。据此,可以更容易的确定合适的阈值,因为此时阈值表示概率而不是绝对差。因此,在背景值波动较大的区域,需要更大的差异来确定前景对象。
当给定的高斯模型没有被足够频繁地匹配时,则认为它并不是背景模型的一部分。相反,当某个像素值在当前维护的背景模型之外(即前景像素)时,就会创建一个新的高斯模型,如果新模型成为最频繁模型,那么它就会与背景相关联。
该算法显然比简单的背景/前景分割器实现起来更复杂。但在 OpenCV
中可以使用 cv::BackgroundSubtractorMOG
实现,它被定义为通用类 cv::BackgroundSubtractor
的子类:
int main () {
// 打开视频
cv::VideoCapture capture("example.avi");
if (!capture.isOpened()) return 0;
// 当前视频帧
cv::Mat frame;
// 前景二值图像
cv::Mat foreground;
// 背景图像
cv::Mat background;
cv::namedWindow("Extracted Foreground");
cv::Ptr<cv::BackgroundSubtractor> ptrMOG = cv::bgsegm::createBackgroundSubtractorMOG();
bool stop(false);
while (!stop) {
if (!capture.read(frame)) break;
// 升级背景并返回前景
ptrMOG->apply(frame, foreground, 0.01);
cv::threshold(foreground, foreground, 128, 255, cv::THRESH_BINARY_INV);
cv::imshow("Extracted Foreground", foreground);
if (cv::waitKey(10) >= 0) stop = true;
}
cv::waitKey();
}
只需创建类实例并调用,算法将同时更新背景并返回前景图像。需要注意的是,此处的背景模型是根据颜色计算的。在 OpenCV
中,还实现了另一种方法,通过检查观察到的像素变化是否仅仅是由局部亮度变化引起的(如果是,则可能是由于阴影)或是否还包括色度变化来识别阴影,可以使用类 cv::BackgroundSubtractorMOG2
调用该算法。该算法可以动态确定要使用的每个像素的适当高斯模型的数量。可以在多个视频上尝试使用以上方法,以观察不同算法的性能。
头文件 (videoprocessor.h
) 完整代码参考视频序列处理一节,头文件 (BGFGSegmentor.h
) 完整代码如下所示:
#if !defined BGFGSeg
#define BGFGSeg
#include
#include
#include
#include "videoprocessor.h"
class BGFGSegmentor : public FrameProcessor {
cv::Mat gray; // 灰度图像
cv::Mat background; // 累积背景
cv::Mat backImage; // 当前背景图像
cv::Mat foreground; // 前景图像
double learningRate; // 学习率
int threshold; // 阈值
public:
BGFGSegmentor() : threshold(10), learningRate(0.01) {}
// 设置阈值
void setThreshold(int t) {
threshold= t;
}
// 设置学习率
void setLearningRate(double r) {
learningRate= r;
}
// processing method
void process(cv:: Mat &frame, cv:: Mat &output) {
// 转换为灰度图像
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
// 初始化背景图像
if (background.empty())
gray.convertTo(background, CV_32F);
background.convertTo(backImage,CV_8U);
// 计算当前图像与背景图像间的差异
cv::absdiff(backImage,gray,foreground);
// 对前景图像应用阈值
cv::threshold(foreground,output,threshold,255,cv::THRESH_BINARY_INV);
// 累积背景
cv::accumulateWeighted(gray, background,
// alpha*gray + (1-alpha)*background
learningRate, // alpha
output); // mask
}
};
#endif
主函数文件 (foreground.cpp
) 完整代码如下所示:
#include
#include
#include
#include
#include
#include "videoprocessor.h"
#include "BGFGSegmentor.h"
int main () {
// 打开视频
cv::VideoCapture capture("r3.mp4");
if (!capture.isOpened()) return 0;
// 当前视频帧
cv::Mat frame;
// 前景二值图像
cv::Mat foreground;
// 背景图像
cv::Mat background;
cv::namedWindow("Extracted Foreground");
cv::Ptr<cv::BackgroundSubtractor> ptrMOG = cv::bgsegm::createBackgroundSubtractorMOG();
bool stop(false);
while (!stop) {
if (!capture.read(frame)) break;
// 升级背景并返回前景
ptrMOG->apply(frame, foreground, 0.01);
cv::threshold(foreground, foreground, 128, 255, cv::THRESH_BINARY_INV);
cv::imshow("Extracted Foreground", foreground);
if (cv::waitKey(10) >= 0) stop = true;
}
cv::waitKey();
// 创建视频处理实例#include
#include
#include
#include
#include
#include "videoprocessor.h"
#include "BGFGSegmentor.h"
int main () {
// 打开视频
cv::VideoCapture capture("r3.mp4");
if (!capture.isOpened()) return 0;
// 当前视频帧
cv::Mat frame;
// 前景二值图像
cv::Mat foreground;
// 背景图像
cv::Mat background;
cv::namedWindow("Extracted Foreground");
cv::Ptr<cv::BackgroundSubtractor> ptrMOG = cv::bgsegm::createBackgroundSubtractorMOG();
bool stop(false);
while (!stop) {
if (!capture.read(frame)) break;
// 升级背景并返回前景
ptrMOG->apply(frame, foreground, 0.01);
cv::threshold(foreground, foreground, 128, 255, cv::THRESH_BINARY_INV);
cv::imshow("Extracted Foreground", foreground);
if (cv::waitKey(10) >= 0) stop = true;
}
cv::waitKey();
// 创建视频处理实例
VideoProcessor processor;
BGFGSegmentor segmentor;
segmentor.setThreshold(25);
// 打开视频文件
processor.setInput("example.avi");
processor.setFrameProcessor(&segmentor);
// 显示视频
processor.displayOutput("Extracted Foreground");
processor.setDelay(1000./processor.getFrameRate());
processor.run();
cv::waitKey();
}
VideoProcessor processor;
BGFGSegmentor segmentor;
segmentor.setThreshold(25);
// 打开视频文件
processor.setInput("example.avi");
processor.setFrameProcessor(&segmentor);
// 显示视频
processor.displayOutput("Extracted Foreground");
processor.setDelay(1000./processor.getFrameRate());
processor.run();
cv::waitKey();
}
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像
OpenCV实战(22)——单应性及其应用
OpenCV实战(23)——相机标定
OpenCV实战(24)——相机姿态估计
OpenCV实战(25)——3D场景重建
OpenCV实战(26)——视频序列处理