OpenCV实战——提取视频中的前景对象

news/2025/1/12 10:56:18/

OpenCV实战——提取视频中的前景对象

    • 0. 前言
    • 1. 提取视频中的前景对象
    • 2. 混合高斯方法
    • 3. 完整代码
    • 相关链接

0. 前言

当固定摄像机观察场景时,背景基本保持不变。在这种情况下,我们真正感兴趣的目标是场景中的移动物体。为了提取这些前景物体,我们需要建立一个背景模型,然后将背景模型与当前帧进行比较,检测前景物体,前景提取是智能监控应用中的基本步骤。

1. 提取视频中的前景对象

如果我们拥有场景的背景图像(即不包含前景对象的帧)可供使用,那么通过简单的图像差异提取当前帧的前景:

cv::absdiff(backgroundImage,currentImage,foreground);

将差异足够高的像素视为前景像素。但是,大多数情况下,背景图像并不容易获得,实际上,很难保证给定图像中不存在前景对象。此外,背景场景通常会随着时间而变化,例如光照条件发生了变化或者背景中添加或移除了对象。
因此,有必要动态构建背景场景的模型,可以通过观察场景一段时间完成。我们假设,大多数情况下,背景的每个像素位置都是可见的,那么简单地计算所有观察值的平均值可能是一个很好的策略。然而,由于多种原因,这并不可行。首先,这将需要在计算背景之前存储大量图像;其次,当我们累积图像来计算平均图像时,无法完成前景提取;同时,无法确定何时以及应该累积多少图像来计算可用的背景模型;此外,前景对象图像会对平均背景的计算产生影响。
更好的策略是构建动态背景模型,这可以通过计算移动平均 (moving average) 来实现。这是一种计算最新接收值的时间信号平均值的方法,如果 p t p_t pt 是给定时间 t t t 的像素值, μ t − 1 μ_{t-1} μt1 是当前平均值,则使用以下公式更新该平均值:
u t = ( 1 − α ) μ t − 1 + α p t u_t=(1-\alpha)\mu_{t-1}+\alpha p_t ut=(1α)μt1+α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 methodvoid 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)*backgroundlearningRate,       // 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),因此前景对象在结果图像中显示为黑色像素。
最后,为简单起见,我们程序构建的背景模型是基于提取的灰度图像帧,计算彩色背景需要在色彩空间中计算移动平均值,主要困难在于确定合适的阈值,以得到更加优秀结果。
上述提取场景中前景物体的方法对于背景相对稳定的简单场景效果很好。然而,在许多情况下,背景场景可能会在某些区域发生波动,从而导致频繁的假前景检测,例如,移动的背景物体(例如风中摇晃的树叶)或炫光效应(例如在水面上反射的阳光)。阴影也会带来一系列问题,因为这些阴影通常被检测为移动对象的一部分。为了解决这些问题,需要引入了更复杂的背景建模方法,例如混合高斯方法。

2. 混合高斯方法

混合高斯方法是在移动平均值的基础上进行改进的算法。首先,该方法为每个像素维护多个移动平均值模型。这样,如果背景像素在两个值之间波动,那么就会存储两个移动平均值。只有当一个新的像素值不属于任何常观察到的模型时,它才会被声明为前景。可以通过使用参数确定模型数量,常用模型数量为 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α)σt12+α(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 调用该算法。该算法可以动态确定要使用的每个像素的适当高斯模型的数量。可以在多个视频上尝试使用以上方法,以观察不同算法的性能。

3. 完整代码

头文件 (videoprocessor.h) 完整代码参考视频序列处理一节,头文件 (BGFGSegmentor.h) 完整代码如下所示:

#if !defined BGFGSeg
#define BGFGSeg#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>#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 methodvoid 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)*backgroundlearningRate,       // alpha output);            // mask}
};#endif

主函数文件 (foreground.cpp) 完整代码如下所示:

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/bgsegm.hpp>#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 <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/bgsegm.hpp>#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)——视频序列处理


http://www.ppmy.cn/news/307181.html

相关文章

PotPlayer结合alist播放网盘视频

目录 1. PotPlayer的下载安装2. 新建专辑3. 测试播放视频总结 欢迎关注 『发现你走远了』 博客&#xff0c;持续更新中 欢迎关注 『发现你走远了』 博客&#xff0c;持续更新中 书接上文 AList挂载工具安装搭建使用教程&#xff0c;快速访问多个网盘的资源&#xff08;保姆级图…

Win系统 - 如何查看电脑有几个内存条插槽?

如何不拆开笔记本后盖的情况下电脑查看有几个内存条插槽。 1、打开“任务管理器”&#xff0c;选择“性能”&#xff0c;点击“内存”&#xff0c;看到右下角“已使用的插槽” 2、鼠标移上去&#xff0c;内存条有详细信息

华为2288HV5服务器内存插法及内存插槽位置

内存插槽位置 2288H V5最多可以安装24条DDR4内存&#xff0c;推荐使用均衡内存配置&#xff0c;可实现最佳内存性能。 CPU1对应的内存槽位上至少配置1条DDR4内存。 图1 内存插槽位置 图2 DDR4内存安装原则&#xff08;1个处理器&#xff09; 图3 DDR4内存安装原则&#xf…

安装笔记本内存条及认识内存插槽

目前主流的笔记本内存规格 目前市场是比较常见的内存条的一般都是DDR2和DDR3这两种。一般都是512MB、1G、2G的。DDR3是目前最主流的。但是也是最贵的笔记本内存条。 笔记本内存条中间有一个小卡口&#xff0c;而且不在正中间&#xff0c;这样能避免插反。我做了个笔记本内存插法…

内存插槽

内存插槽是指主板上所采用的内存插槽类型和数量。主板所支持的内存种类和容量都由内存插槽来决定的。目前主要应用于主板上的内存插槽有&#xff1a; SIMM&#xff08;Single Inline Memory Module&#xff0c;单内联内存模块&#xff09; 168针SIMM插槽 内存条通过金手指与主…

计算机内存插在主板的哪个槽,四个内存插槽,这是正确的安装顺序

具有防呆设计的主板插槽几乎不可能错误地插入. 因此很有可能在第一个插槽中安装了内存. 尽管可以&#xff0c;但是会阻止内存在最佳状态下工作. 主板手册中有这样一句话: 说明 为便于理解&#xff0c;首先对内存插槽编号&#xff0c;从靠近CPU插槽的位置开始: 主板 针对不同情况…

惠普暗夜精灵2怎么打开后盖_#原创新人# 拆机加内存 — HP 惠普 暗影精灵Ⅱ代 开箱...

#原创新人# 拆机加内存 — HP 惠普 暗影精灵Ⅱ代 开箱 2016-06-18 11:08:10 36点赞 19收藏 23评论 小编注:此篇文章来自即可瓜分10万金币,周边好礼达标就有,邀新任务奖励无上限,点击查看活动详情 之前一直用台式,后来实在跑不动了,打算在配台的,又想一直没用笔记本想试试…

关于内存及其参数和插槽类型

一、内存的分类&#xff1a; 1) SDRAM:Synchronous Dynamic Random Access Memory&#xff0c;同步动态随机存储器。 同步是指 Memory工作需要同步时钟&#xff0c;内部的命令的发送与数据的传输都以它为基准&#xff1b;动态是指存储阵列需要不断的刷新来保证数据不丢失&…