简介

  码书法,是对每个像素建立一个记录背景范围的码书,一个码书上包含多个表示背景范围的盒子,类似于颜色空间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 TRANSPOSEcv::transpose(frame, frame);
#endif // TRANSPOSEcv::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 TRANSPOSEcv::transpose(frame, frame);
#endif // TRANSPOSEcv::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;
}

效果

左图为原视频帧,中图为前景提取图,右图为前景清理逼近后的图



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

  1. 背景提取算法——帧间差分法、背景差分法、ViBe算法、ViBe+算法

    背景提取是在视频图像序列中提取出背景,背景就是场景中静止不动的景物.因为摄像机不动,因此图像中的每个像素点都有一个对应的背景值,在一段时间内,这个背景值是比较固定的.背景提取的目标就是根据视频图像序列 ...

  2. OpenCV视频分析背景提取与前景提取

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 基本思想 OpenCV中支持的两种背景提取算法都是基于模型密度评估 ...

  3. CyberLink YouCam 新人求教一个绿色背景提取红色小球的问题

    http://www.opencv.org.cn/forum.php?mod=viewthread&tid=36980&page=1 http://blog.csdn.net/chen ...

  4. OpenCV—python 视频分析背景提取与前景提取

    文章目录 一.算法 二.代码 MOG2(Mixture of Gaussian) 与 KNN对比 Kmeans 行人检测代码 OpenCV中支持的两种背景提取算法都是 基于模型密度评估,然后在 像素级 ...

  5. ViBe 背景提取算法原理

    本文由原始文档的部分章节翻译得到: ViBe: A universal background subtraction algorithm for video sequences Olivier Bar ...

  6. OpenCV3学习(10.2)codebook码书法实现背景剔除

    一.概述 码本模型是 Kim 等提出的一种新颖的背景建模方法.该模型针对彩色视频图像序列,根据像素点的连续采样值的颜色失真程度及其亮度范围,将背景像素用码本表示,然后利用背景差分法思想对新输入像素值与 ...

  7. css动画唯美背景,小码哥-利用CSS3渐变实现唯美背景图

    原标题:小码哥-利用CSS3渐变实现唯美背景图 CSS3 渐变(gradients)可以让你在两个或多个指定的颜色之间显示平稳的过渡. 以前,你必须使用图像来实现这些效果.但是,通过使用 CSS3 渐 ...

  8. 星空唯美浮雕效果背景源码

    简介: 一个非常好的PSD源代码,用于寂寞星空的美丽背景. 孤独与沉重相结合,孤独感凝视着宇宙中尘世的夜晚. 戈壁沙漠给图片增添了些神秘感. 网盘下载地址: http://kekewl.org/pyt ...

  9. 网上流行的护眼背景对照码,十六进制,RGB值

    护眼背景 可以有效防止长时间面对显示器,造成的视觉疲劳. CSS(十六进制) #CCEED0 RBG(红,绿,蓝) 204,238,208 HSL 色度:85,饱和度:120,亮度:208

最新文章

  1. 点对点信道互连以太网实验_汽车以太网 – 引领汽车IVN向多速以太网过渡
  2. 关于子元素的margin-top对父级容器无效
  3. 百度工程师亲授CCF BDCI解题思路,15万大奖等你来拿
  4. Boost:自定义树的测试程序
  5. 数据结构——基于字符串模式匹配算法的病毒感染检测
  6. PyQt5代码触发QTreeWidget的列表中某个item点击信号
  7. CentOS7 shell脚本安装jdk
  8. itchat没了咱们还可以用Python的wxpy模块来玩转微信
  9. linux下配置mysql默认编码utf8
  10. 中缀表达式转后缀表达式 java_中缀表达式转后缀表达式并计算结果Java实现
  11. android markdown 框架,Android Studio MarkDown风格README的正确打开姿势
  12. 纯个人整理!分享珍藏很久的《手把手AI项目》合集手册(附链接)
  13. Luogu P1131 [ZJOI2007]时态同步 树形DP
  14. 决策树CART介绍*
  15. 【MySQL基础 安装】CentOS 7 Yum网络部署 最新官方MySQL5 2020_2_1
  16. 新手学习电脑知识的一些方法 oldtimeblog
  17. 自我成长的9个小贴士
  18. php ar技术,vr和ar是什么技术,有什么区别
  19. 胆结石饮食有什么禁忌?
  20. 哪里可以学Java?

热门文章

  1. Helium白皮书 2018-11版
  2. LogicalDOC含有中文字符的各种报错
  3. Maxwell-Boltzmann分布函数的推导
  4. Android SVG动画详细例子
  5. Kotlin基础教程
  6. 计算机机房使用面积怎么算,如何建设电子计算机机房(一)
  7. SqlSession 同步为注册,因为同步未激活
  8. 《天才基本法》中的P=NP?问题对应算法逻辑
  9. 《深入浅出RxJS》读书笔记
  10. tib_tiqu_without_temperature