OpenCV ViBe 运动检测算法实现
ViBe(Visual Background Extractor)是一种高效的像素级背景建模算法,特别适用于实时运动检测。以下是完整的OpenCV ViBe实现。
ViBe算法原理
ViBe算法的核心思想是为每个像素维护一个样本集,通过比较当前像素值与样本集中的值来判断是否为前景。算法特点包括:
- 快速初始化:仅需第一帧即可建立背景模型
- 在线更新:检测同时不断更新背景模型
- 内存占用小:每个像素只存储有限样本
- 实时性好:计算简单,适合实时应用
完整实现代码
1. ViBe.h 头文件
#pragma once
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;
// ViBe算法参数定义
#define NUM_SAMPLES 20 // 每个像素点的样本个数
#define MIN_MATCHES 2 // 最小匹配数阈值
#define RADIUS 20 // 半径阈值(颜色距离)
#define SUBSAMPLE_FACTOR 16 // 子采样概率(1/16)
class ViBe_BGS {
public:
ViBe_BGS(void); // 构造函数
~ViBe_BGS(void); // 析构函数
void init(const Mat& _image); // 初始化
void processFirstFrame(const Mat& _image); // 第一帧处理
void testAndUpdate(const Mat& _image); // 测试和更新
Mat getMask(void) { return m_mask; } // 获取前景掩码
private:
Mat m_samples[NUM_SAMPLES]; // 每个像素的样本集
Mat m_foregroundMatchCount; // 前景匹配计数
Mat m_mask; // 前景掩码
// 邻居点偏移量(3x3邻域)
int c_xoff[9] = {-1, 0, 1, -1, 1, -1, 0, 1, 0};
int c_yoff[9] = {-1, 0, 1, -1, 1, -1, 0, 1, 0};
};
2. ViBe.cpp 实现文件
#include "ViBe.h"
#include <random>
#include <ctime>
// 构造函数
ViBe_BGS::ViBe_BGS(void) {
// 初始化随机数种子
srand(static_cast<unsigned int>(time(nullptr)));
}
// 析构函数
ViBe_BGS::~ViBe_BGS(void) {
// 清理资源
for (int i = 0; i < NUM_SAMPLES; i++) {
m_samples[i].release();
}
m_foregroundMatchCount.release();
m_mask.release();
}
// 初始化函数
void ViBe_BGS::init(const Mat& _image) {
// 检查输入图像
if (_image.empty()) {
cerr << "Error: Input image is empty!" << endl;
return;
}
// 初始化样本集
for (int i = 0; i < NUM_SAMPLES; i++) {
m_samples[i] = Mat::zeros(_image.size(), CV_8UC1);
}
// 初始化前景匹配计数
m_foregroundMatchCount = Mat::zeros(_image.size(), CV_8UC1);
// 初始化前景掩码
m_mask = Mat::zeros(_image.size(), CV_8UC1);
}
// 处理第一帧,建立初始背景模型
void ViBe_BGS::processFirstFrame(const Mat& _image) {
int rows = _image.rows;
int cols = _image.cols;
// 遍历每个像素
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 为每个像素的每个样本随机选择邻域像素
for (int k = 0; k < NUM_SAMPLES; k++) {
// 随机选择邻域位置
int random = rand() % 9;
int row = i + c_yoff[random];
int col = j + c_xoff[random];
// 边界检查
if (row < 0) row = 0;
if (row >= rows) row = rows - 1;
if (col < 0) col = 0;
if (col >= cols) col = cols - 1;
// 将邻域像素值作为样本
m_samples[k].at<uchar>(i, j) = _image.at<uchar>(row, col);
}
}
}
}
// 测试当前帧并更新背景模型
void ViBe_BGS::testAndUpdate(const Mat& _image) {
int rows = _image.rows;
int cols = _image.cols;
// 遍历每个像素
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
uchar pixel = _image.at<uchar>(i, j);
int count = 0;
int index = 0;
// 统计与样本的匹配数
while ((count < MIN_MATCHES) && (index < NUM_SAMPLES)) {
uchar sample = m_samples[index].at<uchar>(i, j);
// 计算颜色距离
int dist = abs(static_cast<int>(pixel) - static_cast<int>(sample));
if (dist < RADIUS) {
count++;
}
index++;
}
// 判断是否为背景
if (count >= MIN_MATCHES) {
// 背景像素
m_foregroundMatchCount.at<uchar>(i, j) = 0;
m_mask.at<uchar>(i, j) = 0;
// 随机更新背景模型
int random = rand() % SUBSAMPLE_FACTOR;
if (random == 0) {
// 随机选择一个样本进行更新
random = rand() % NUM_SAMPLES;
m_samples[random].at<uchar>(i, j) = pixel;
// 随机更新邻域像素的样本
random = rand() % 9;
int row = i + c_yoff[random];
int col = j + c_xoff[random];
// 边界检查
if (row < 0) row = 0;
if (row >= rows) row = rows - 1;
if (col < 0) col = 0;
if (col >= cols) col = cols - 1;
// 更新邻域像素的随机样本
random = rand() % NUM_SAMPLES;
m_samples[random].at<uchar>(row, col) = pixel;
}
} else {
// 前景像素
m_foregroundMatchCount.at<uchar>(i, j)++;
m_mask.at<uchar>(i, j) = 255;
// 如果连续多次被检测为前景,则更新为背景
if (m_foregroundMatchCount.at<uchar>(i, j) > 50) {
int random = rand() % SUBSAMPLE_FACTOR;
if (random == 0) {
random = rand() % NUM_SAMPLES;
m_samples[random].at<uchar>(i, j) = pixel;
}
}
}
}
}
}
3. 改进版ViBe实现(支持彩色图像)
// ViBe_Color.h
#pragma once
#include <opencv2/opencv.hpp>
#include <vector>
#include <random>
using namespace cv;
using namespace std;
class ViBe_Color {
public:
ViBe_Color(int numSamples = 20, int radius = 20, int minMatches = 2);
~ViBe_Color();
void initialize(const Mat& frame);
void apply(const Mat& frame, Mat& foregroundMask);
void getBackgroundModel(Mat& background);
private:
int m_numSamples; // 样本数量
int m_radius; // 半径阈值
int m_minMatches; // 最小匹配数
vector<Mat> m_samples; // 样本集
Mat m_background; // 背景模型
RNG m_rng; // 随机数生成器
// 邻居点偏移
vector<Point> m_neighbors;
void initNeighbors();
double colorDistance(const Vec3b& p1, const Vec3b& p2);
};
// ViBe_Color.cpp
#include "ViBe_Color.h"
#include <cmath>
ViBe_Color::ViBe_Color(int numSamples, int radius, int minMatches)
: m_numSamples(numSamples), m_radius(radius), m_minMatches(minMatches) {
initNeighbors();
}
ViBe_Color::~ViBe_Color() {
m_samples.clear();
}
void ViBe_Color::initNeighbors() {
// 8邻域
m_neighbors = {
Point(-1, -1), Point(0, -1), Point(1, -1),
Point(-1, 0), Point(0, 0), Point(1, 0),
Point(-1, 1), Point(0, 1), Point(1, 1)
};
}
double ViBe_Color::colorDistance(const Vec3b& p1, const Vec3b& p2) {
// 计算欧氏距离
double dist = 0;
for (int i = 0; i < 3; i++) {
dist += (p1[i] - p2[i]) * (p1[i] - p2[i]);
}
return sqrt(dist);
}
void ViBe_Color::initialize(const Mat& frame) {
if (frame.empty()) return;
// 清空样本集
m_samples.clear();
// 初始化样本集
for (int i = 0; i < m_numSamples; i++) {
m_samples.push_back(Mat::zeros(frame.size(), CV_8UC3));
}
// 初始化背景模型
m_background = Mat::zeros(frame.size(), CV_8UC3);
// 为每个像素建立初始样本集
for (int y = 0; y < frame.rows; y++) {
for (int x = 0; x < frame.cols; x++) {
for (int k = 0; k < m_numSamples; k++) {
// 随机选择邻域像素
int idx = m_rng.uniform(0, static_cast<int>(m_neighbors.size()));
Point neighbor = m_neighbors[idx];
int nx = x + neighbor.x;
int ny = y + neighbor.y;
// 边界检查
nx = max(0, min(nx, frame.cols - 1));
ny = max(0, min(ny, frame.rows - 1));
// 设置样本值
m_samples[k].at<Vec3b>(y, x) = frame.at<Vec3b>(ny, nx);
}
}
}
}
void ViBe_Color::apply(const Mat& frame, Mat& foregroundMask) {
if (frame.empty() || m_samples.empty()) return;
// 创建前景掩码
foregroundMask = Mat::zeros(frame.size(), CV_8UC1);
// 遍历每个像素
for (int y = 0; y < frame.rows; y++) {
for (int x = 0; x < frame.cols; x++) {
Vec3b currentPixel = frame.at<Vec3b>(y, x);
int matches = 0;
// 检查与样本的匹配
for (int k = 0; k < m_numSamples && matches < m_minMatches; k++) {
Vec3b sample = m_samples[k].at<Vec3b>(y, x);
if (colorDistance(currentPixel, sample) < m_radius) {
matches++;
}
}
// 判断前景/背景
if (matches >= m_minMatches) {
// 背景像素
foregroundMask.at<uchar>(y, x) = 0;
// 随机更新背景模型
if (m_rng.uniform(0, 16) == 0) { // 1/16概率
// 更新当前像素的样本
int k = m_rng.uniform(0, m_numSamples);
m_samples[k].at<Vec3b>(y, x) = currentPixel;
// 随机更新邻域像素的样本
int idx = m_rng.uniform(0, static_cast<int>(m_neighbors.size()));
Point neighbor = m_neighbors[idx];
int nx = x + neighbor.x;
int ny = y + neighbor.y;
if (nx >= 0 && nx < frame.cols && ny >= 0 && ny < frame.rows) {
k = m_rng.uniform(0, m_numSamples);
m_samples[k].at<Vec3b>(ny, nx) = currentPixel;
}
}
} else {
// 前景像素
foregroundMask.at<uchar>(y, x) = 255;
}
}
}
// 形态学操作去除噪声
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
morphologyEx(foregroundMask, foregroundMask, MORPH_OPEN, kernel);
morphologyEx(foregroundMask, foregroundMask, MORPH_CLOSE, kernel);
}
void ViBe_Color::getBackgroundModel(Mat& background) {
if (!m_samples.empty()) {
// 计算背景模型(样本均值)
background = Mat::zeros(m_samples[0].size(), CV_8UC3);
for (int y = 0; y < background.rows; y++) {
for (int x = 0; x < background.cols; x++) {
Vec3f sum(0, 0, 0);
for (int k = 0; k < m_numSamples; k++) {
Vec3b sample = m_samples[k].at<Vec3b>(y, x);
sum[0] += sample[0];
sum[1] += sample[1];
sum[2] += sample[2];
}
background.at<Vec3b>(y, x) = Vec3b(
static_cast<uchar>(sum[0] / m_numSamples),
static_cast<uchar>(sum[1] / m_numSamples),
static_cast<uchar>(sum[2] / m_numSamples)
);
}
}
}
}
4. 主程序示例
// main.cpp - 灰度图像版本
#include "ViBe.h"
#include <iostream>
#include <chrono>
using namespace std;
using namespace cv;
int main(int argc, char* argv[]) {
// 打开视频文件或摄像头
VideoCapture cap;
if (argc > 1) {
cap.open(argv[1]); // 从文件读取
} else {
cap.open(0); // 从摄像头读取
}
if (!cap.isOpened()) {
cerr << "Error: Cannot open video source!" << endl;
return -1;
}
// 创建ViBe对象
ViBe_BGS vibe;
Mat frame, gray, mask;
int frameCount = 0;
// 设置显示窗口
namedWindow("Input", WINDOW_AUTOSIZE);
namedWindow("Foreground", WINDOW_AUTOSIZE);
namedWindow("Background", WINDOW_AUTOSIZE);
// 处理视频帧
while (true) {
cap >> frame;
if (frame.empty()) break;
frameCount++;
// 转换为灰度图
cvtColor(frame, gray, COLOR_BGR2GRAY);
// 第一帧初始化
if (frameCount == 1) {
vibe.init(gray);
vibe.processFirstFrame(gray);
cout << "ViBe model initialized with first frame." << endl;
} else {
// 处理后续帧
auto start = chrono::high_resolution_clock::now();
vibe.testAndUpdate(gray);
auto end = chrono::high_resolution_clock::now();
// 获取前景掩码
mask = vibe.getMask();
// 计算处理时间
auto duration = chrono::duration_cast<chrono::milliseconds>(end - start);
cout << "Frame " << frameCount << " processed in " << duration.count() << " ms" << endl;
// 形态学操作去除噪声
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
morphologyEx(mask, mask, MORPH_OPEN, kernel);
morphologyEx(mask, mask, MORPH_CLOSE, kernel);
// 显示结果
imshow("Input", frame);
imshow("Foreground", mask);
// 显示背景(通过掩码)
Mat background;
gray.copyTo(background, 255 - mask);
imshow("Background", background);
}
// 按键控制
char key = waitKey(30);
if (key == 27 || key == 'q') { // ESC或q退出
break;
} else if (key == 'p') { // p暂停
waitKey(0);
} else if (key == 's') { // s保存当前帧
imwrite("foreground_" + to_string(frameCount) + ".png", mask);
cout << "Foreground saved as foreground_" << frameCount << ".png" << endl;
}
}
cap.release();
destroyAllWindows();
cout << "ViBe processing completed. Total frames: " << frameCount << endl;
return 0;
}
5. 彩色版本主程序
// main_color.cpp - 彩色图像版本
#include "ViBe_Color.h"
#include <iostream>
#include <chrono>
using namespace std;
using namespace cv;
int main() {
// 打开摄像头
VideoCapture cap(0);
if (!cap.isOpened()) {
cerr << "Error: Cannot open camera!" << endl;
return -1;
}
// 创建彩色ViBe对象
ViBe_Color vibe_color(20, 30, 2); // 20个样本,半径30,最小匹配2
Mat frame, foreground, background;
bool initialized = false;
namedWindow("Input", WINDOW_AUTOSIZE);
namedWindow("Foreground", WINDOW_AUTOSIZE);
namedWindow("Background Model", WINDOW_AUTOSIZE);
cout << "Press SPACE to initialize ViBe model" << endl;
cout << "Press ESC to exit" << endl;
while (true) {
cap >> frame;
if (frame.empty()) break;
if (!initialized) {
// 显示提示
putText(frame, "Press SPACE to initialize", Point(10, 30),
FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
} else {
// 处理帧
auto start = chrono::high_resolution_clock::now();
vibe_color.apply(frame, foreground);
auto end = chrono::high_resolution_clock::now();
// 获取背景模型
vibe_color.getBackgroundModel(background);
// 计算FPS
auto duration = chrono::duration_cast<chrono::milliseconds>(end - start);
double fps = 1000.0 / duration.count();
// 显示信息
string info = "FPS: " + to_string(fps).substr(0, 4);
putText(frame, info, Point(10, 30),
FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
}
// 显示结果
imshow("Input", frame);
if (initialized) {
imshow("Foreground", foreground);
imshow("Background Model", background);
}
// 按键处理
char key = waitKey(30);
if (key == 27) { // ESC退出
break;
} else if (key == 32 && !initialized) { // SPACE初始化
vibe_color.initialize(frame);
initialized = true;
cout << "ViBe model initialized!" << endl;
} else if (key == 'r' && initialized) { // r重置
vibe_color.initialize(frame);
cout << "ViBe model re-initialized!" << endl;
}
}
cap.release();
destroyAllWindows();
return 0;
}
6. CMakeLists.txt 编译配置
cmake_minimum_required(VERSION 3.10)
project(ViBe_Motion_Detection)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 查找OpenCV
find_package(OpenCV REQUIRED)
# 包含目录
include_directories(${OpenCV_INCLUDE_DIRS})
# 灰度版本
add_executable(vibe_gray
ViBe.cpp
main.cpp
)
# 彩色版本
add_executable(vibe_color
ViBe_Color.cpp
main_color.cpp
)
# 链接OpenCV库
target_link_libraries(vibe_gray ${OpenCV_LIBS})
target_link_libraries(vibe_color ${OpenCV_LIBS})
算法参数调优指南
关键参数说明
| 参数 | 默认值 | 说明 | 调整建议 |
|---|---|---|---|
NUM_SAMPLES |
20 | 每个像素的样本数量 | 值越大,背景模型越稳定,但内存占用增加 |
MIN_MATCHES |
2 | 最小匹配数阈值 | 值越大,检测越严格,漏检率降低但误检率可能增加 |
RADIUS |
20 | 颜色距离阈值 | 值越大,对光照变化越鲁棒,但可能漏检小变化 |
SUBSAMPLE_FACTOR |
16 | 背景更新概率 | 值越大,更新越慢,背景适应速度越慢 |
不同场景的参数建议
-
室内监控:
NUM_SAMPLES: 15-20RADIUS: 15-25MIN_MATCHES: 2-3
-
室外交通监控:
NUM_SAMPLES: 20-30RADIUS: 25-35MIN_MATCHES: 2-4
-
快速运动场景:
NUM_SAMPLES: 10-15RADIUS: 10-20SUBSAMPLE_FACTOR: 8-12
性能优化技巧
- 多线程处理:
// 使用OpenCV并行处理
parallel_for_(Range(0, rows), const Range& range {
for (int i = range.start; i < range.end; i++) {
// 处理每一行
}
});
- GPU加速:
// 使用CUDA或OpenCL加速
cv::cuda::GpuMat gpu_frame, gpu_gray, gpu_mask;
cv::cuda::cvtColor(gpu_frame, gpu_gray, COLOR_BGR2GRAY);
- 分辨率调整:
// 降低分辨率提高速度
cv::resize(frame, small_frame, Size(), 0.5, 0.5, INTER_LINEAR);
应用示例
行人检测
// 行人检测示例
void detectPedestrians(const Mat& foreground, vector<Rect>& pedestrians) {
// 查找轮廓
vector<vector<Point>> contours;
findContours(foreground, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
for (const auto& contour : contours) {
Rect rect = boundingRect(contour);
double aspect_ratio = static_cast<double>(rect.width) / rect.height;
// 行人通常有特定的宽高比
if (rect.area() > 500 && aspect_ratio > 0.2 && aspect_ratio < 0.8) {
pedestrians.push_back(rect);
}
}
}
车辆检测
// 车辆检测示例
void detectVehicles(const Mat& foreground, vector<Rect>& vehicles) {
vector<vector<Point>> contours;
findContours(foreground, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
for (const auto& contour : contours) {
Rect rect = boundingRect(contour);
double aspect_ratio = static_cast<double>(rect.width) / rect.height;
// 车辆通常有特定的宽高比
if (rect.area() > 1000 && aspect_ratio > 1.2 && aspect_ratio < 3.0) {
vehicles.push_back(rect);
}
}
}
编译和运行
Linux/Mac编译
# 创建构建目录
mkdir build && cd build
# 配置CMake
cmake ..
# 编译
make
# 运行灰度版本
./vibe_gray [video_file]
# 运行彩色版本
./vibe_color
Windows编译(Visual Studio)
- 创建新的CMake项目
- 将上述文件添加到项目中
- 配置OpenCV路径
- 编译运行
参考代码 Opencv的ViBe运动检测 www.youwenfan.com/contentcnt/122343.html
算法优缺点
优点
- 快速初始化:仅需一帧即可建立背景模型
- 内存效率高:每个像素只存储有限样本
- 实时性好:计算复杂度低,适合实时应用
- 适应性强:能适应缓慢的背景变化
- 参数少:只有几个关键参数需要调整
缺点
- 对快速光照变化敏感
- 可能产生鬼影(ghosting)
- 需要手动调整参数
- 对动态背景(如摇曳的树木)处理不佳
改进方向
- 自适应阈值:根据场景动态调整
RADIUS参数 - 多尺度处理:在不同分辨率上运行ViBe
- 阴影去除:结合颜色信息去除阴影
- 背景融合:与其他背景建模算法(如MOG2)结合
- GPU加速:利用GPU并行计算提高速度
浙公网安备 33010602011771号