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 背景更新概率 值越大,更新越慢,背景适应速度越慢

不同场景的参数建议

  1. 室内监控

    • NUM_SAMPLES: 15-20
    • RADIUS: 15-25
    • MIN_MATCHES: 2-3
  2. 室外交通监控

    • NUM_SAMPLES: 20-30
    • RADIUS: 25-35
    • MIN_MATCHES: 2-4
  3. 快速运动场景

    • NUM_SAMPLES: 10-15
    • RADIUS: 10-20
    • SUBSAMPLE_FACTOR: 8-12

性能优化技巧

  1. 多线程处理
// 使用OpenCV并行处理
parallel_for_(Range(0, rows), const Range& range {
    for (int i = range.start; i < range.end; i++) {
        // 处理每一行
    }
});
  1. GPU加速
// 使用CUDA或OpenCL加速
cv::cuda::GpuMat gpu_frame, gpu_gray, gpu_mask;
cv::cuda::cvtColor(gpu_frame, gpu_gray, COLOR_BGR2GRAY);
  1. 分辨率调整
// 降低分辨率提高速度
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)

  1. 创建新的CMake项目
  2. 将上述文件添加到项目中
  3. 配置OpenCV路径
  4. 编译运行

参考代码 Opencv的ViBe运动检测 www.youwenfan.com/contentcnt/122343.html

算法优缺点

优点

  1. 快速初始化:仅需一帧即可建立背景模型
  2. 内存效率高:每个像素只存储有限样本
  3. 实时性好:计算复杂度低,适合实时应用
  4. 适应性强:能适应缓慢的背景变化
  5. 参数少:只有几个关键参数需要调整

缺点

  1. 对快速光照变化敏感
  2. 可能产生鬼影(ghosting)
  3. 需要手动调整参数
  4. 对动态背景(如摇曳的树木)处理不佳

改进方向

  1. 自适应阈值:根据场景动态调整RADIUS参数
  2. 多尺度处理:在不同分辨率上运行ViBe
  3. 阴影去除:结合颜色信息去除阴影
  4. 背景融合:与其他背景建模算法(如MOG2)结合
  5. GPU加速:利用GPU并行计算提高速度
posted @ 2026-04-10 09:47  修BUG狂人  阅读(2)  评论(0)    收藏  举报