ViBe是近几年比较火的一种背景建模算法,该算法主要有以下四个特点:
第一,以历史数据作为背景模型:每个位置的背景模型实质是历史像素值。
第二,随机性:背景模型的初始化和更新方式依概率进行。
第三,邻域操作:当前位置的背景模型初始化时,数据采集自邻域像素;当前位置的背景模型进行更新时,会扩散到邻域。
第四,纯整型运算,无浮点数运算,运算效率高。
该算法的流程也特别简单:
初始化阶段:用一帧画面进行背景建模,每个位置的背景模型从该位置的邻域中进行多次随机采样得到。
匹配和更新阶段:计算每个位置的像素和该位置的背景模型样本值的距离,如果距离小于某个阈值的背景模型样本数大于某个阈值,则认为当前位置为背景,否则为前景。如果当前位置为前景,则以一定的概率更新该位置的某个背景模型样本值,并且以一定的概率更新某个邻域的背景模型的某个样本值。
具体的过程在原作者发表在IEEE Transactions on Image Processing中的论文的附录中已有很详细的说明,简单易懂,这里不再重复。
ViBe的代码实现起来也比较简单。在CSDN上也有不少博主给出了源代码,我注意到好几个版本的代码在效率方面都有明显的不足。最主要的问题在于访问读入图片的像素和背景模型样本值的方法上。有的作者用cv::Mat::at()函数遍历图像中的每个像素,造成处理时间较长,体现不出ViBe算法的时间优势。下面给出我的一个实现代码,并且在代码中给出了比较详细的分析注释。
这个版本的代码完全采用指针操作访问读入图片的像素和背景模型样本值,保证了效率。我的台式机的CPU是i3-2120,开发环境是vs2010,OpenCV是2.4.5的32位版,下面的三个文件编译好,处理320x240像素的彩色图和灰度图,在release模式下,一帧的运行时间基本都在5毫秒以下。
另外,我在实现随机数时,既没有采用C标准库中的函数,也没有采用OpenCV的随机数,而是用时钟周期计数自累加取模操作实现。经过我的实验发现,采用真正的随机数或者这里给出的实现方式,得到的前景检测结果没有明显的差别。
#pragma once
#include
#include
/*!
基于存储历史像素样本值和邻域样本值更新策略的前景提取算法
ViBe: A Universal Background Subtraction Algorithm for Video Sequences
in IEEE Transactions on Image Processing, June, 2011, Volume 20, Issue 6
*/
class ViBe
{
public:
//! 配置参数, 结构体中的参数的含义参见论文
struct Config
{
//! 获取处理彩色图的参数
static Config getRGBConfig(void)
{
return Config("[rgb]", 20, 60, 2, 16);
}
//! 获取处理灰度图的参数
static Config getGrayConfig(void)
{
return Config("[gray]", 20, 20, 2, 16);
}
//! 构造函数
Config(const std::string& label_, int numOfSamples_, int minMatchDist_, int minNumOfMatchCount_, int subSampleInterval_)
: label(label_), numOfSamples(numOfSamples_), minMatchDist(minMatchDist_),
minNumOfMatchCount(minNumOfMatchCount_), subSampleInterval(subSampleInterval_)
{}
std::string label; ///< 标签
int numOfSamples; ///< 每个像素保存的样本数量
int minMatchDist; ///< 处理图片的高度
int minNumOfMatchCount; ///< 判定为背景的最小匹配成功次数
int subSampleInterval; ///< 它的倒数等于更新保存像素值的概率
};
//! 初始化模型
/*!
传入第一帧画面 image, 给定配置参数 config
image 必须是 CV_8UC1 或者 CV_8UC3 格式, 否则会抛出 std::exception 类型的异常
*/
void init(const cv::Mat& image, const Config& config);
//! 提取前景, 更新模型
/*!
结合现有模型参数, 检测输入图片 image 中的前景, 输出到 foregroundImage 中
image 的尺寸和格式必须和 init 函数中进行初始化的图片的尺寸和格式完全相同, 否则会抛出 cv::exception 类型的异常
foregroundImage 的尺寸和 image 相同, 格式为 CV_8UC1, 前景像素值等于 255, 背景像素值等于 0
*/
void update(const cv::Mat& image, cv::Mat& foregroundImage);
private:
int imageWidth; ///< 处理图片的宽度
int imageHeight; ///< 处理图片的高度
int imageChannels; ///< 处理图片的通道数, 支持 1 和 3
int imageType; ///< 处理图片的类型, 支持 CV_8UC1 和 CV_8UC3
cv::Mat samples; ///< 背景模型
std::vector
rowSamples; ///< 样本的行首地址, 使用 vector 方便管理内存
unsigned char** ptrSamples; ///< &rowSamples[0], 使用数组的下标而不是 vector 的 [] 运算符, 加快程序运行速度
void fill8UC3(const cv::Mat& image);
void fill8UC1(const cv::Mat& image);
void proc8UC3(const cv::Mat& image, cv::Mat& foreImage);
void proc8UC1(const cv::Mat& image, cv::Mat& foreImage);
int numOfSamples; ///< 每个像素保存的样本数量
int minMatchDist; ///< 判定前景背景的距离
int minNumOfMatchCount; ///< 判定为背景的最小匹配成功次数
int subSampleInterval; ///< 它的倒数等于更新保存像素值的概率
};
#include
#include
#include
#include
#include
#include "ViBe.h" using namespace std; using namespace cv; // 用于表示当前位置像素八邻域的所有位置的所在行和所在列相对于当前行和当前列的偏移量 // 举个例子, 假设当前像素所在行和所在列是 y 和 x, // 那么该位置上方的点使用的偏移量是 adjPositions[2][], // 即行和列是 y + adjPositions[1][0], x + adjPositions[1][1] // 其他位置的点一次类推 // 8 个邻域点的排列顺序是从左上角开始按逆时针的方向旋转一周 const static int adjPositions[8][2] = {{-1, -1}, {-1, 0}, {-1, 1}, {0, -1}, {0, 1}, {1, -1}, {1, 0}, {1, 1}}; const static int numOfNeighbors = 8; const static int mask = 65535; void ViBe::init(const Mat& image, const Config& config) { CV_Assert(image.cols > 0 && image.rows > 0 && (image.type() == CV_8UC3 || image.type() == CV_8UC1)); imageWidth = image.cols; imageHeight = image.rows; imageChannels = image.channels(); imageType = image.type(); numOfSamples = config.numOfSamples; minMatchDist = config.minMatchDist; minNumOfMatchCount = config.minNumOfMatchCount; subSampleInterval = config.subSampleInterval; // 分配保存样本空间 samples = Mat::zeros(imageWidth * imageHeight * imageChannels * numOfSamples, 1, CV_8UC1); rowSamples.resize(imageHeight, 0); // 标记图片每一行首个像素存储样本的地址 for (int i = 0; i < imageHeight; i++) rowSamples[i] = samples.data + imageWidth * numOfSamples * imageChannels * i; ptrSamples = &rowSamples[0]; // 填充背景模型 if (imageChannels == 3) fill8UC3(image); else if (imageChannels == 1) fill8UC1(image); } void ViBe::fill8UC3(const Mat& image) { // 用 CPU 时钟计数模拟随机数 int index = getTickCount() & mask; // 输入图片的相邻行首地址, 用于快速定位当前行邻域所在行 const unsigned char* ptrImageAdjRows[3]; for (int i = 1; i < imageHeight - 1; i++) { ptrImageAdjRows[0] = image.ptr
(i - 1); ptrImageAdjRows[1] = image.ptr
(i); ptrImageAdjRows[2] = image.ptr
(i + 1); // ptrCenterRow[0] 指向要处理的那一行, 即当前行, // ptrCenterRow[-1] 指向当前行的上一行 // ptrCenterRow[1] 指向当前行的下一行 const unsigned char** ptrCenterRow = &ptrImageAdjRows[1]; for (int j = 1; j < imageWidth - 1; j++) { for (int k = 0; k < numOfSamples; k++) { // index 值自增后取模, 确保 index 的值落在 0 到 numOfNeighbors - 1 之间 index = (++index) % numOfNeighbors; // 当前像素所在行是 i, 所在列是 j // ptrSamples[i] + (j * numOfSamples + k) * 3 指向当前像素第 k 个存储样本的第 0 个字节 // 第 k 个样本存储下标为 index 的邻域的像素值 memcpy(ptrSamples[i] + (j * numOfSamples + k) * 3, ptrCenterRow[adjPositions[index][0]] + (j + adjPositions[index][1]) * 3, sizeof(unsigned char) * 3); } } } } void ViBe::fill8UC1(const Mat& image) { int index = getTickCount() & mask; const unsigned char* ptrImageAdjRows[3]; for (int i = 1; i < imageHeight - 1; i++) { ptrImageAdjRows[0] = image.ptr
(i - 1); ptrImageAdjRows[1] = image.ptr
(i); ptrImageAdjRows[2] = image.ptr
(i + 1); const unsigned char** ptrCenterRow = &ptrImageAdjRows[1]; for (int j = 1; j < imageWidth - 1; j++) { for (int k = 0; k < numOfSamples; k++) { index = (++index) % numOfNeighbors; ptrSamples[i][j * numOfSamples + k] = ptrCenterRow[adjPositions[index][0]][j + adjPositions[index][1]]; } } } } void ViBe::update(const Mat& image, Mat& foreImage) { CV_Assert(image.type() == imageType && image.cols == imageWidth && image.rows == imageHeight); foreImage.create(imageHeight, imageWidth, CV_8UC1); // 画面最外面一圈的像素默认为背景 // 本算法的前景检测和背景模型更新过程涉及到邻域操作 // 最外面一圈像素的邻域是不完整的, 如果要进行处理, 需要额外的判断 // 但是在实际的应用中, 画面不会太小, 最外面一圈像素的识别结果不会对最终的结果产生显著的影响 // 所以直接把这一圈像素忽略掉了 foreImage.row(0).setTo(0); foreImage.row(imageHeight - 1).setTo(0); foreImage.col(0).setTo(0); foreImage.col(imageWidth - 1).setTo(0); // 按照实际的图片类型进行处理 if (imageChannels == 3) proc8UC3(image, foreImage); else if (imageChannels == 1) proc8UC1(image, foreImage); } void ViBe::proc8UC3(const Mat& image, Mat& foreImage) { // 匹配成功后, 确定是否替换当前位置背景模型的某个样本值 int rndReplaceCurr = getTickCount() & mask; // 要替换的背景模型样本值的下标 int rndIndexCurr = getTickCount() & mask; // 匹配成功后, 确定是否替换当前位置某个邻域的某个样本值 int rndReplaceAdj = getTickCount() & mask; // 要替换的邻域的下标 int rndPositionAdj = getTickCount() & mask; // 要替换的背景模型样本值的下标 int rndIndexAdj = getTickCount() & mask; // 首尾两行默认为背景, 不做处理 for (int i = 1; i < imageHeight - 1; i++) { const unsigned char* ptrRow = image.ptr
(i); unsigned char* ptrFore = foreImage.ptr
(i); // 首尾两列默认为背景, 不做处理 for (int j = 1; j < imageWidth - 1; j++) { // 统计当前像素能够和多少个已存储的样本匹配 int matchCount = 0; const unsigned char* ptrInput = ptrRow + j * 3; unsigned char* ptrCurrSamples = ptrSamples[i] + j * numOfSamples * 3; for (int k = 0; k < numOfSamples && matchCount < minNumOfMatchCount; k++) { int dist = abs(int(ptrInput[0]) - int(ptrCurrSamples[k * 3])) + abs(int(ptrInput[1]) - int(ptrCurrSamples[k * 3 + 1])) + abs(int(ptrInput[2]) - int(ptrCurrSamples[k * 3 + 2])); if (dist < minMatchDist) matchCount++; } // 是前景 if (matchCount < minNumOfMatchCount) { ptrFore[j] = 255; continue; } // 是背景 ptrFore[j] = 0; // 更新当前像素的存储样本, 以 1 / subSampleInterval 为概率更新背景模型的某个样本值 rndReplaceCurr = (++rndReplaceCurr) % subSampleInterval; if (rndReplaceCurr == 0) { // 确定要替换的样本值的下标 rndIndexCurr = (++rndIndexCurr) % numOfSamples; // 赋值 memcpy(ptrCurrSamples + rndIndexCurr * 3, ptrInput, sizeof(unsigned char) * 3); } // 更新某个邻域像素的存储样本, , 以 1 / subSampleInterval 为概率更新背景模型的某个样本值 rndReplaceAdj = (++rndReplaceAdj) % subSampleInterval; if (rndReplaceAdj == 0) { // 确定要替换的邻域的下标 rndPositionAdj = (++rndPositionAdj) % numOfNeighbors; // 确定要替换的样本值的下标 rndIndexAdj = (++rndIndexAdj) % numOfSamples; // 邻域所在行 int y = i + adjPositions[rndPositionAdj][0]; // 邻域所在列 int x = j + adjPositions[rndPositionAdj][1]; // 赋值 memcpy(ptrSamples[y] + (x * numOfSamples + rndIndexAdj) * 3, ptrInput, sizeof(unsigned char) * 3); } } } } void ViBe::proc8UC1(const Mat& image, Mat& foreImage) { int rndReplaceCurr = getTickCount() & mask; int rndIndexCurr = getTickCount() & mask; int rndReplaceAdj = getTickCount() & mask; int rndPositionAdj = getTickCount() & mask; int rndIndexAdj = getTickCount() & mask; for (int i = 1; i < imageHeight - 1; i++) { const unsigned char* ptrRow = image.ptr
(i); unsigned char* ptrFore = foreImage.ptr
(i); for (int j = 1; j < imageWidth - 1; j++) { // 统计当前像素能够和多少个已存储的样本匹配 int matchCount = 0; const unsigned char* ptrInput = ptrRow + j; unsigned char* ptrCurrSamples = ptrSamples[i] + j * numOfSamples; for (int k = 0; k < numOfSamples && matchCount < minNumOfMatchCount; k++) { int dist = abs(int(ptrInput[0]) - int(ptrCurrSamples[k])); if (dist < minMatchDist) matchCount++; } // 是前景 if (matchCount < minNumOfMatchCount) { ptrFore[j] = 255; continue; } // 是背景 ptrFore[j] = 0; // 更新当前像素的存储样本 rndReplaceCurr = (++rndReplaceCurr) % subSampleInterval; if (rndReplaceCurr == 0) { rndIndexCurr = (++rndIndexCurr) % numOfSamples; ptrCurrSamples[rndIndexCurr] = *ptrInput; } // 更新邻域像素的存储样本 rndReplaceAdj = (++rndReplaceAdj) % subSampleInterval; if (rndReplaceAdj == 0) { rndPositionAdj = (++rndPositionAdj) % numOfNeighbors; rndIndexAdj = (++rndIndexAdj) % numOfSamples; int y = i + adjPositions[rndPositionAdj][0]; int x = j + adjPositions[rndPositionAdj][1]; ptrSamples[y][x * numOfSamples + rndIndexAdj] = *ptrInput; } } } }
#include
#include
#include
#include
#include
#include "ViBe.h" using namespace std; using namespace cv; int main(int argc, char* argv[]) { string path = "D:/video.flv"; VideoCapture cap; cap.open(path); if (!cap.isOpened()) { printf("could not open file %s\n", path.c_str()); return -1; } bool hasInit = false; long long int beg, end; double freq = getTickFrequency(); int frameCount; ViBe vibe, vibeGray; Size size(320, 240); Mat frame; Mat image, fore; Mat imageGray, foreGray; while (true) { if (!cap.read(frame)) break; resize(frame, image, size); cvtColor(image, imageGray, CV_BGR2GRAY); if (!hasInit) { hasInit = true; frameCount = 0; vibe.init(image, ViBe::Config::getRGBConfig()); vibeGray.init(imageGray, ViBe::Config::getGrayConfig()); imshow("image", image); imshow("imageGray", imageGray); } else { frameCount++; printf("[%4d]: ", frameCount); beg = getTickCount(); vibe.update(image, fore); end = getTickCount(); printf("color = %.4f ms, ", (end - beg) / freq * 1000); beg = getTickCount(); vibeGray.update(imageGray, foreGray); end = getTickCount(); printf("gray = %.4f ms\n", (end - beg) / freq * 1000); imshow("image", image); imshow("imageGray", imageGray); imshow("fore", fore); imshow("fore gray", foreGray); if (waitKey(20) == 'q') break; } } return 0; }