背景提取——码书法(codebook),一些总结和改进

简介

  码书法,是对每个像素建立一个记录背景范围的码书,一个码书上包含多个表示背景范围的盒子,类似于颜色空间YUV(背景颜色变化多发生在亮度轴V上)上漂浮着的气团。如果新像素和先前建立的盒子足够近,则扩展盒子。否则,新建新的盒子。为了能在存在移动的前景时进行背景学习,需要更低频率地(为了更好地学习背景和避免清理掉背景盒子)清理码书中的被判定为前景的盒子。最后进行背景差分,不在码书中全部盒子范围内的为前景,否则,为背景。
  由于不可避免的存在各种干扰,需要使用形态学操作和连通分量法进行处理,最后用多边形逼近或者凸包逼近绘出处理后的前景结果。

结构

  • 码书类继承自std::vector,加上记录当前帧数的成员变量t。
  • 码数单元(盒子),包括盒子上下边界(max,min)、学习阈上下边界(learnHigh,learnLow)、最后一次被击中帧数(t_last_update)、盒子创建时帧数(t_create)(自己加的成员变量,改进stale计算方式)、用于判断长时间未被击中(stale)。

codeBookh.h

#pragma once
#include <iostream>
#include <opencv.hpp>
using namespace std;

//#define TRANSPOSE

#define CHANNELS 3
#define DP_EPSILON_DENOMINATOR 100.0
#define CVCLOSE_KSIZE 5 //闭操作核大小
#define CVOPEN_ITR 1//腐蚀膨胀次数

string videoname = "test2.mp4";
unsigned int bound[3] = { 10,10,10 };//像素边框,用于调整学习阈
int minMod[3] = { 5,5,5 };//误差范围,用于检测前景
int maxMod[3] = { 5,5,5 };

int fpr = 300;//每轮学习帧数
int fpsScale = 2;//每隔几帧取一帧计算

int rsb = 8;//图像缩小倍数
bool p1h0 = true;
int deleNum = 0;//调用清理盒子次数

float minAreaScale = 0.005;//最小面积比例

/*
码书单元(盒子)
*/
class CodeElement {
public:
	uchar learnHigh[CHANNELS];//学习阈值上限
	uchar learnLow[CHANNELS];//学习阈值下限
	uchar max[CHANNELS];//盒子上边界
	uchar min[CHANNELS];//盒子下边界
	int t_last_update;//最后一次被命中的时间戳
	int stale;//反映历史最长没命中此盒子的时间段,time(此时)-t_last_update>stale便更新,越大表示此盒子越不是背景(干扰或移动前景)
	int t_create;//创建时间戳
	CodeElement() {
		for (int i = 0; i < CHANNELS; i++)
			learnHigh[i] = learnLow[i] = max[i] = min[i] = 0;
		t_last_update = t_create = 0;
	}

	CodeElement& operator=(const CodeElement& ce) {
		for (int i = 0; i < CHANNELS; i++) {
			learnHigh[i] = ce.learnHigh[i];
			learnLow[i] = ce.learnLow[i];
			max[i] = ce.max[i];
			min[i] = ce.min[i];
		}
		t_last_update = ce.t_last_update;
		t_create = ce.t_create;
		stale = ce.stale;
		return *this;
	}

	CodeElement(const CodeElement& ce) { *this = ce; }
};

/*
码书,每个像素上对应一个码书
*/
class CodeBook :public std::vector<CodeElement> {//vector太低效,可用MAX_CODE来限制最大码书大小,静态分配数组CodeElement[MAX_CODES]
public:
	int t;//累加的点的个数
	CodeBook() { t = 0; }
	CodeBook(int n) :std::vector<CodeElement>(n) { t = 0; }//构建有n个码书单元的码书,更新出直接用book[i]
};

int updateCodebook(const cv::Vec3b& p, CodeBook& c, unsigned* cbBounds, int numChannels);
int clearStaleEntries(CodeBook& c);
uchar backgroundDiff(const cv::Vec3b& p, CodeBook& c, int numChannels, int* minMod, int* maxMod);
void findConnectedComponents(cv::Mat& mask, std::vector<cv::Rect>& bbs, std::vector<cv::Point>& centers, float minArea, bool poly1_hull0 = 1);

背景学习

  由于在背景学习时,stale只是t-t_last_update并不能很好的表示此盒子在一轮视频流中未被击中的情况。这样会导致在盒子清理时,视频流偏后面出现的移动前景不能被清理掉。所以,此处修改为t - t_last_update + t_create。
  每个像素自带一个自定义大小的边框,用于在创建新盒子时设置学习阈,或者是在扩展旧盒子时调整学习阈。

#include "pch.h"
#include "codeBookh.h"

/*
更新码书,返回更新的码书单元索引
p:输入的YUV像素,如果只看亮度,此处做修改
c:此像素的码书
cbBounds:码书边框,通常{10,10,10}
numChannels:像素维度,如果小于3,默认各通道YUV
*/
int updateCodebook(const cv::Vec3b& p, CodeBook& c, unsigned* cbBounds, int numChannels) {
	//计算像素的上下边框位置
	unsigned int high[3], low[3], n;
	for (n = 0; n < numChannels; n++) {
		high[n] = p[n] + *(cbBounds + n);
		high[n] = high[n] > 255 ? 255 : high[n];//用于调整学习阈
		low[n] = p[n] - *(cbBounds + n);
		low[n] = low[n] < 0 ? 0 : low[n];//用于调整学习阈
	}
	
	//判断是否在已有盒子学习阈内
	int i;//盒子索引
	int matchChannel;//所有维度上满足学习阈内的维度数
	for (i = 0; i < c.size(); i++) {
		matchChannel = 0;//遍历每个盒子先清零
		//遍历每个维度
		for (n = 0; n < numChannels; n++) {
			if ((c[i].learnLow[n] <= p[n]) && (p[n] <= c[i].learnHigh[n]))
				matchChannel++;
		}
		//判断是否在此盒子内
		if (matchChannel == numChannels) {
			c[i].t_last_update = c.t;//更新盒子时间戳
			//调整盒子边界
			for (n = 0; n < numChannels; n++) {
				c[i].max[n] = c[i].max[n] < p[n] ? p[n] : c[i].max[n];
				c[i].min[n] = c[i].min[n] > p[n] ? p[n] : c[i].min[n];
			}
			break;//成功找到满足的盒子,返回盒子索引,不需要创建新盒子
		}
	}

	//维护码书中各盒子的状态信息
	for (int s = 0; s < c.size(); s++) {
		int negRun = c.t - c[s].t_last_update + c[s].t_create;
		c[s].stale = c[s].stale < negRun ? negRun : c[s].stale;
	}
	//cout << endl;

	//判断是否需要创建新盒子
	if (i == c.size()) {
		CodeElement ce;
		for (n = 0; n < numChannels; n++) {
			ce.learnHigh[n] = high[n];
			ce.learnLow[n] = low[n];
			ce.max[n] = p[n];
			ce.min[n] = p[n];
		}
		ce.t_last_update = c.t;
		ce.t_create = c.t;
		ce.stale = 0;
		c.push_back(ce);
	}

	//缓慢调整盒子学习阈
	for (n = 0; n < numChannels; n++) {
		c[i].learnHigh[n] += c[i].learnHigh[n] < high[n] ? 1 : 0;
		c[i].learnLow[n] -= c[i].learnLow[n] > low[n] ? 1 : 0;
	}
	return i;
}

清理前景盒子

  在改进了stale的计算方式后,可以将一轮学习中偏后面出现的移动前景也清理掉。
  除此以外,若是在此处将t_last_update等归零,会使得多轮学习能不断习得新的背景,但也可能误将前景当做背景,且会在帧数过少时清理掉背景盒子;
  不清零则第一轮习得背景盒子后只会扩展或减少,不会增加,若存在一个长时间不动的前景物体被模型习得为背景,当此前景物体移走时,会出现一个空洞。但是不清零能更有效去掉前景盒子,综合利用多轮训练的视频流,且不会在帧数过少时清掉背景盒子。
  为了解决以上问题,不能等周期调用此函数,要以更低频率调用此函数。

/*
删去学习过程中很少被命中的盒子
可用于在有少量前景浮动的视频流学习背景
返回删掉的盒子数
c:一个像素的码书
*/
int clearStaleEntries(CodeBook& c) {
	int staleThresh = c.t >> 1;//整个视频帧数的一半,即一半以上未击中则为背景
	int* keep = new int[c.size()];//标记每个盒子是否要删除
	int keepCnt = 0;//保留的盒子数,用于调整盒子数量vector来删除之后的全部盒子

	//标记每个盒子是否常用
	for (int i = 0; i < c.size(); i++) {
		if (c[i].stale > staleThresh) {
			keep[i] = 0;
		}
		else
		{
			keep[i] = 1;
			keepCnt += 1;
		}
	}

	//调整盒子索引,保留的在前,去掉后面的
	int k = 0;//保留索引
	int numCleared = 0;//记录删除掉的盒子数
	for (int ii = 0; ii < c.size(); ii++) {
		if (keep[ii]) {
			c[k] = c[ii];
			//c[k].t_last_update = 0;//清零使得多轮学习能不断习得背景,不清零则第一轮习得背景盒子后等周期训练盒子会只减不增
			//c[k].t_create = 0;//但是不清零能更有效去掉前景盒子,且不会在帧数过少时清掉背景盒子
			k++;
		}
		else {
			numCleared++;
		}
	}
	c.resize(keepCnt);
	//c.t = 0;
	delete[] keep;
	return numCleared;
}

检测前景

  在检测前景时,附加了一个自定义大小的允许偏离的阈值(maxMod,minMod)。在判断是否在某盒子内时,允许其上下有一定的偏离。再此偏离之外才是候选前景。

/*
检测前景物体
背景返回0,前景返回255
p:传入像素(YUV)
c:此像素的码书
numChannels:通道数
minMod:允许的误差下限范围
maxMod:允许的误差上限范围
*/
uchar backgroundDiff(const cv::Vec3b& p, CodeBook& c, int numChannels, int* minMod, int* maxMod) {
	int matchChannel;
	int i;
	//判断是否在误差范围内满足某个盒子
	for (i = 0; i < c.size(); i++) {
		matchChannel = 0;
		for (int n = 0; n < numChannels; n++) {
			if ((c[i].min[n] - minMod[n] <= p[n]) && (p[n] <= c[i].max[n] + maxMod[n])) {
				matchChannel++;
			}
			else
			{
				break;
			}
		}
		if (matchChannel == numChannels)
			break;
	}
	if (i >= c.size())
		return 255;
	return 0;
}

连通分量前景清理

/*
使用连通分量进行清理
先开运算去小噪声,再闭运算重建开运算中丢失的区域,对小连通分量标记和剔除,寻找前景轮廓并用多边形或者凸包逼近
mask:传入的8位单通道图像
bbs:边框矩形
centers:质心
minArea:允许的最小连通分量
poly1_hull0:默认1,前景轮廓多边形逼近。0,凸包逼近
*/
void findConnectedComponents(cv::Mat& mask,  std::vector<cv::Rect>& bbs, std::vector<cv::Point>& centers, float minArea, bool poly1_hull0) {
	//开闭运算
	cv::morphologyEx(mask, mask, cv::MORPH_OPEN, cv::Mat(), cv::Point(-1, -1), CVOPEN_ITR);
	cv::morphologyEx(mask, mask, cv::MORPH_CLOSE, cv::getStructuringElement(cv::MORPH_RECT,cv::Size(CVCLOSE_KSIZE, CVCLOSE_KSIZE)), cv::Point(-1, -1), CVOPEN_ITR);
	//cv::imshow("morph", mask);
	//剔除小连通分量
	cv::Mat labels, stats, centroids;
	int num=cv::connectedComponentsWithStats(mask, labels, stats, centroids);//stats:N*5,[x0,y0,width0,height0,area0;	centroids:N*2,[cx0,xy0;
	std::vector<int> colors(num);
	colors[0] = 0;//背景
	for (int i = 1; i < num; i++) {
		if (stats.at<int>(i, cv::CC_STAT_AREA) < minArea) {
			colors[i] = 0;//小连通分量颜色为黑
		}
		else
		{
			colors[i] = 255;
			cv::Rect rect = cv::Rect(stats.at<int>(i, cv::CC_STAT_LEFT), stats.at<int>(i, cv::CC_STAT_TOP), stats.at<int>(i, cv::CC_STAT_WIDTH), stats.at<int>(i, cv::CC_STAT_HEIGHT));
			bbs.push_back(rect);
			cv::Point pot = cv::Point(centroids.at<double>(i, 0), centroids.at<double>(i, 1));
			centers.push_back(pot);
		}
	}
	mask.setTo(0);
	for (int r = 0; r < mask.rows; r++) {
		for (int c = 0; c < mask.cols; c++) {
			int label = labels.at<int>(r, c);
			mask.at<uchar>(r, c) = colors[label];
		}
	}

	//逼近
	std::vector<std::vector<cv::Point>> contours_all, contours;
	cv::findContours(mask, contours_all, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
	for (int i = 0; i < contours_all.size(); i++) {
		int len = cv::arcLength(contours_all[i], true);
		std::vector<cv::Point> c_new;
		if (poly1_hull0) {
			cv::approxPolyDP(contours_all[i], c_new, len/DP_EPSILON_DENOMINATOR, true);
		}
		else
		{
			cv::convexHull(contours_all[i], c_new);
		}
		contours.push_back(c_new);
	}
	mask.setTo(0);
	cv::drawContours(mask, contours, -1, 255, -1);
}

调参

  我们在视频流上使用updateCodeBook()学习背景模型后,用clearStaleEntries()清理模型,周期性更新模型,并以更低的频率周期性清理(多轮可以排查掉在一轮更新时前期后期都出现的同一前景)

int main()
{
	cv::VideoCapture cap;
	cv::Mat frame, frame_YUV;
	CodeBook* cb;
	cv::Vec3b p;
	
	cap.open(videoname);
	cap >> frame;
	int len = frame.rows*frame.cols / (rsb*rsb);
	cb = new CodeBook[len];
	
	int n = 1, fnum = 1;
	//学习背景
	for (;;) {
		cap >> frame;
		if (frame.empty())
			break;

		//周期性取帧计算
		if (fnum != fpsScale) {
			fnum++;
			continue;
		}
		else {
			fnum = 0;
		}
#ifdef TRANSPOSE
		cv::transpose(frame, frame);
#endif // TRANSPOSE

		cv::resize(frame, frame, frame.size() / rsb);
		cv::imshow("src", frame);
		if (cv::waitKey(33) >= 0)
			break;

		cv::cvtColor(frame, frame_YUV, cv::COLOR_BGR2YUV);

		for (int row = 0; row < frame.rows; row++) {
			for (int col = 0; col < frame.cols; col++) {
				int n = row * frame.cols + col;
				cb[n].t += 1;
				p = frame_YUV.at<cv::Vec3b>(row, col);
				updateCodebook(p, cb[n], bound, CHANNELS);
			}
		}
		
		//以更低频率周期性地清理
		if (n != fpr) {
			n++;
		}
		else {
			n = 0;
			fpr*=2;
			//清除码书中不常用的盒子
			for (int l = 0; l < len; l++) {
				int num = clearStaleEntries(cb[l]);
				cout << "delete:" << num << endl;
			}
			//cout << ++deleNum << endl;
		}
	}
	cap.release();

	//清除码书中不常用的盒子
	for (int l = 0; l < len; l++) {
		int num = clearStaleEntries(cb[l]);
		//cout << "delete:" << num << endl;
	}
	cout << ++ deleNum << endl;

	cap.open(videoname);
	//检测前景
	for (;;) {
		cap >> frame;
		if (frame.empty())
			break;
#ifdef TRANSPOSE
		cv::transpose(frame, frame);
#endif // TRANSPOSE
		cv::resize(frame, frame, frame.size() / rsb);
		if (cb->size() <= 0)
		{
			cerr << "没有学习到的码书" << endl;
			system("pause");
			return 0;
		}

		cv::cvtColor(frame, frame_YUV, cv::COLOR_BGR2YUV);
		cv::Mat dst = cv::Mat::zeros(frame.size(), CV_8U);
		for (int row = 0; row < frame.rows; row++) {
			for (int col = 0; col < frame.cols; col++) {
				int n = row * frame.cols + col;
				//cb[n].t += 1;
				p = frame_YUV.at<cv::Vec3b>(row, col);
				uchar d = backgroundDiff(p, cb[n], CHANNELS, minMod, maxMod);
				dst.at<uchar>(row, col) = d;
			}
		}
		cv::imshow("dst", dst);

		//使用连通分量进行前景清理
		std::vector<cv::Rect> bbs;//前景框
		std::vector<cv::Point> centers;//前景质心
		findConnectedComponents(dst, bbs, centers, dst.rows*dst.cols*minAreaScale, p1h0);
		for (int i = 0; i < bbs.size(); i++) {
			cv::rectangle(frame, bbs[i], cv::Scalar(0, 0, 255));
			cv::circle(frame, centers[i], 2, cv::Scalar(0, 0, 255), -1);
		}
		cv::imshow("src", frame);
		cv::imshow("polyDst", dst);
		if (cv::waitKey(33) >= 0)
			break;
	}
	return 0;
}

效果

左图为原视频帧,中图为前景提取图,右图为前景清理逼近后的图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值