模型使用的时候,一定要从微观考虑,即将每个点作为小模型考虑。因为种种策略最后都是应用到每个点上去的。这点思维转换一定要有,初学图形图像方面的,尤其是初用opencv的总是养成直接从每个图每个图考虑的思维,总喜欢将策略实施到一副图上,入门的时候是这样的,因为都是直接使用函数。而且如果仅仅是使用模型的话,其实也是只看到根据每个图每个图更新,似乎也是从全局出发的。但是阅读模型源代码的时候,要从点的角度出发考虑。无论是GMM的模型还是基于此篇论文构建的模型均是如此。
具体到此篇模型。出发点就是根据后验概率推导出如何判断一个点是否是前景点的公式。但是接下来的问题就是如何表达出概率。采用的策略是对RGB值得直方图进行统计,而且为了减少统计量,首先对RGB进行了分层。然后对前N个直方量计数,这样就更减少了计算量。N的数值跟RGB的分层数量有关,但是具体如何计算目前不知道。
考虑到了前后帧间的变化明显时候确定此点变为了动点,但同时考虑到背景的渐变的变化。因而需要维护两个表。(这一段不满意,再想想后改)
因而随后便是做完两个差分后,判断出前景点。然后,根据更新策略,对这一时刻的各个点的概率进行更新。
而这一模型设计到两类概率:先验概率和似然概率,因为只有这两个才能算出点是背景还是前景的后验概率。
先说先验概率,由于模型有两个,先验概率也是有两个:即CvBGPixelStat中的Pbc和Pbcc,分别对应两个模型。
再说后验概率,这里面涉及到了统计,因而每个模型都维护了各自的一个表。表的数量便是需要N2,而其中前N1的量是每次都要统计的。而N1-N2是接受新背景时候的缓冲区域。更新的时候,如果未找到可以匹配当前点的量,那么加入这个点到末尾N2,然后排序后就到了前面,N2指向的内存其实已经变化,因而完成对新背景点的接受。如果找到了与当前点匹配的点,那么便更新取得此点的概率值。
判断是否是前景点的出发角度:先假设此点是背景点,根据假设背景点的变化范围不会太大,找出可以接收此点的背景区域的概率(即每个可以接受它的背景点的概率之和),然后根据得到的公式计算便可得出。#define MIN_PV 1E-10 /*宏定义 使后面访问方便 这个可以学习参考*/ #define V_C(k,l) ctable[k].v[l] #define PV_C(k) ctable[k].Pv #define PVB_C(k) ctable[k].Pvb #define V_CC(k,l) cctable[k].v[l] #define PV_CC(k) cctable[k].Pv #define PVB_CC(k) cctable[k].Pvb // Function cvUpdateFGDStatModel updates statistical model and returns number of foreground regions // parameters: // curr_frame - current frame from video sequence // p_model - pointer to CvFGDStatModel structure /*进入正题 更新*/ static int CV_CDECL icvUpdateFGDStatModel( IplImage* curr_frame, CvFGDStatModel* model ) { int mask_step = model->Ftd->widthStep; CvSeq *first_seq = NULL, *prev_seq = NULL, *seq = NULL; //定义队列结构 为后面取出前景的轮廓准备 IplImage* prev_frame = model->prev_frame; int region_count = 0; int FG_pixels_count = 0; //cvRound四舍五入 根据分层数量求相应方差 默认的是Lc=2 delta默认为2 // delta取值2的原因 个人理解是 分层后的相邻向量的变化均可接受 设256个分为64层 那么0-7中任意两两之间的变化都可认为是邻近变化 int deltaC = cvRound(model->params.delta * 256 / model->params.Lc); int deltaCC = cvRound(model->params.delta * 256 / model->params.Lcc); int i, j, k, l; //clear storages cvClearMemStorage(model->storage); cvZero(model->foreground); // From foreground pixel candidates using image differencing // with adaptive thresholding. The algorithm is from: // // Thresholding for Change Detection // Paul L. Rosin 1998 6p // http://www.cis.temple.edu/~latecki/Courses/CIS750-03/Papers/thresh-iccv.pdf // /*先分别求两个图的差分 当前和前一副图差分(帧间差分Ftd) 当前图和截止上一幅为止的背景进行差分(背景差分Fbd)*/ cvChangeDetection( prev_frame, curr_frame, model->Ftd ); cvChangeDetection( model->background, curr_frame, model->Fbd ); /*以下进行背景和前景的划分*/ for( i = 0; i < model->Ftd->height; i++ ) { for( j = 0; j < model->Ftd->width; j++ ) { if( ((uchar*)model->Fbd->imageData)[i*mask_step+j] || ((uchar*)model->Ftd->imageData)[i*mask_step+j] )//点的两个差分中只要有一个表标注 { float Pb = 0; float Pv = 0; float Pvb = 0; /*一下三个分别取得相应点的相应变量(存储概率的状态变量和存储特征向量统计的两个表)的入口地址*/ CvBGPixelStat* stat = model->pixel_stat + i * model->Ftd->width + j; CvBGPixelCStatTable* ctable = stat->ctable; CvBGPixelCCStatTable* cctable = stat->cctable; uchar* curr_data = (uchar*)(curr_frame->imageData) + i*curr_frame->widthStep + j*3; uchar* prev_data = (uchar*)(prev_frame->imageData) + i*prev_frame->widthStep + j*3; int val = 0; // Is it a motion pixel? if( ((uchar*)model->Ftd->imageData)[i*mask_step+j] )//如果是帧间差分有较大变化 进行以下处理 注意所用的特征向量 { if( !stat->is_trained_dyn_model ) { val = 1; } else { // Compare with stored CCt vectors: //以下根据原论文2.3的 sum(p(vi|(b,s)) i from 1 to N1 注释没法编辑公式 只能表达意思了 公式看论文更好 //这里同时考虑到了学习速率 这儿是alpha2 即接受新背景的速率 见参数定义 //要注意的是截止到这个else结束均是对一个点进行处理 for( k = 0; PV_CC(k) > model->params.alpha2 && k < model->params.N1cc; k++ ) { if ( abs( V_CC(k,0) - prev_data[0]) <= deltaCC && abs( V_CC(k,1) - prev_data[1]) <= deltaCC && abs( V_CC(k,2) - prev_data[2]) <= deltaCC && abs( V_CC(k,3) - curr_data[0]) <= deltaCC && abs( V_CC(k,4) - curr_data[1]) <= deltaCC && abs( V_CC(k,5) - curr_data[2]) <= deltaCC) { //此处不相乘而相加 //个人认为可以看做采用求最大似然概率时候取ln的方法 //计算机中累乘后精度大大降低 Pv += PV_CC(k); //似然概率p(vi|s) 即点在此时刻取到此向量的概率 Pvb += PVB_CC(k);//似然概率p(vi|b,s) 假设点为背景点的情况下 在此时刻取到此向量的概率 } } Pb = stat->Pbcc; //先验概率 即点是背景的概率 if( 2 * Pvb * Pb <= Pv ) val = 1; //根据一系列推导的结果 推导过程见原论文的2.2 判断词典是否为前景点 } } else if( stat->is_trained_st_model ) //若是点的背景差分发生较大变化 与帧间差分处理的理论完全相同 只是采用的特征向量不同 { // Compare with stored Ct vectors: for( k = 0; PV_C(k) > model->params.alpha2 && k < model->params.N1c; k++ ) { if ( abs( V_C(k,0) - curr_data[0]) <= deltaC && abs( V_C(k,1) - curr_data[1]) <= deltaC && abs( V_C(k,2) - curr_data[2]) <= deltaC ) { Pv += PV_C(k); Pvb += PVB_C(k); } } Pb = stat->Pbc; if( 2 * Pvb * Pb <= Pv ) val = 1; //判断是否为前景点 } // Update foreground: ((uchar*)model->foreground->imageData)[i*mask_step+j] = (uchar)(val*255); //更新前景(二值图) FG_pixels_count += val;//更新前景点的数量 } // end if( change detection... } // for j... } // for i... //end BG/FG classification // Foreground segmentation. // Smooth foreground map: //形态学的开闭运算 //先腐蚀后膨胀的过程称为开运算 消除小物体 平滑边界 //先膨胀后腐蚀的过程称为闭运算 填充孔洞 平滑边界 //两者均不改变面积 if( model->params.perform_morphing ){ cvMorphologyEx( model->foreground, model->foreground, 0, 0, CV_MOP_OPEN, model->params.perform_morphing ); cvMorphologyEx( model->foreground, model->foreground, 0, 0, CV_MOP_CLOSE, model->params.perform_morphing ); } //进行筛选 去除不满足最小面积和有孔洞要求的前景 if( model->params.minArea > 0 || model->params.is_obj_without_holes ){ // Discard under-size foreground regions: // cvFindContours( model->foreground, model->storage, &first_seq, sizeof(CvContour), CV_RETR_LIST ); //一下是简单的单链表的操作 应当无需解释 for( seq = first_seq; seq; seq = seq->h_next ) { CvContour* cnt = (CvContour*)seq; if( cnt->rect.width * cnt->rect.height < model->params.minArea || (model->params.is_obj_without_holes && CV_IS_SEQ_HOLE(seq)) ) { // Delete under-size contour: prev_seq = seq->h_prev; if( prev_seq ) { prev_seq->h_next = seq->h_next; if( seq->h_next ) seq->h_next->h_prev = prev_seq; } else { first_seq = seq->h_next; if( seq->h_next ) seq->h_next->h_prev = NULL; } } else { region_count++; } } model->foreground_regions = first_seq; cvZero(model->foreground); //根据得到的轮廓画出前景 cvDrawContours(model->foreground, first_seq, CV_RGB(0, 0, 255), CV_RGB(0, 0, 255), 10, -1); } else { model->foreground_regions = NULL; } // Check ALL BG update condition: //前景点的数量达到要求后 认为所有点已经得到足够数据 既已经可以近似认为特征向量构成的直方图的前N1大的概率之和满足2.3的公式(5) if( ((float)FG_pixels_count/(model->Ftd->width*model->Ftd->height)) > CV_BGFG_FGD_BG_UPDATE_TRESH ) { for( i = 0; i < model->Ftd->height; i++ ) for( j = 0; j < model->Ftd->width; j++ ) { CvBGPixelStat* stat = model->pixel_stat + i * model->Ftd->width + j; stat->is_trained_st_model = stat->is_trained_dyn_model = 1; } } // Update background model: //更新背景 for( i = 0; i < model->Ftd->height; i++ ) { for( j = 0; j < model->Ftd->width; j++ ) { CvBGPixelStat* stat = model->pixel_stat + i * model->Ftd->width + j; CvBGPixelCStatTable* ctable = stat->ctable; CvBGPixelCCStatTable* cctable = stat->cctable; uchar *curr_data = (uchar*)(curr_frame->imageData)+i*curr_frame->widthStep+j*3; uchar *prev_data = (uchar*)(prev_frame->imageData)+i*prev_frame->widthStep+j*3; if( ((uchar*)model->Ftd->imageData)[i*mask_step+j] || !stat->is_trained_dyn_model ) //对帧间差分发生了变化的点进行处理 { float alpha = stat->is_trained_dyn_model ? model->params.alpha2 : model->params.alpha3; float diff = 0; int dist, min_dist = 2147483647, indx = -1; //update Pb //更新策略 Pbcc = (1-alpha)*Pbcc + M*alpha 如果点在此刻是前景点 那么M取0 即此点位背景点的概率降低 否则增加 //此为先验概率P(b|s)的更新策略 stat->Pbcc *= (1.f-alpha); if( !((uchar*)model->foreground->imageData)[i*mask_step+j] ) { stat->Pbcc += alpha; } // Find best Vi match: //表中每个分量进行更新 //看做对似然概率更新 //PV_CC(k) = PV_CC(k)*(1-alpaha) + M*alpha 若匹配更新取到此点的概率 //PVB_CC(k) = PV_BCC(k)*(1-alpaha) + (M^B)*alpha 若匹配且为背景点时更新,即在点为背景的条件下取得此向量的概率 for(k = 0; PV_CC(k) && k < model->params.N2cc; k++ ) { // Exponential decay of memory PV_CC(k) *= (1-alpha); PVB_CC(k) *= (1-alpha); if( PV_CC(k) < MIN_PV ) //小于最小阈值 归0 continue进行下一个k的处理 { PV_CC(k) = 0; PVB_CC(k) = 0; continue; } dist = 0; //已经存储的N2cc个向量中 寻找可以匹配当前向量的最合适的点 //注意此时相减用的是RGB的值,但是考虑的时候是从分层之后的角度考虑的,因而判断变化的接收可否的阈值采用的是2*256/Lcc for( l = 0; l < 3; l++ ) //六个分量中的任何一个变化较大均不考虑后续处理 { int val = abs( V_CC(k,l) - prev_data[l] ); if( val > deltaCC ) break; dist += val; val = abs( V_CC(k,l+3) - curr_data[l] ); if( val > deltaCC) break; dist += val; } if( l == 3 && dist < min_dist ) { min_dist = dist; indx = k; } } if( indx < 0 ) { // Replace N2th elem in the table by new feature: //如果没有可以匹配当前向量的 //那么对cc表的最后一个向量进行重新赋值 indx = model->params.N2cc - 1; PV_CC(indx) = alpha; PVB_CC(indx) = alpha; //udate Vt for( l = 0; l < 3; l++ ) { V_CC(indx,l) = prev_data[l]; V_CC(indx,l+3) = curr_data[l]; } } else { // Update: //如果存在可以匹配的向量,那么更新参数 //增加取得此向量的概率 PV_CC(indx) += alpha; if( !((uchar*)model->foreground->imageData)[i*mask_step+j] ) { PVB_CC(indx) += alpha; } } //re-sort CCt table by Pv //对于截止匹配点的排列进行更新,因为匹配点后面的点的值仍然会小于之前的点,只有匹配点的概率发生了变化 //如果这次没有匹配,那么最后一个点排序后会到前面,也就是下次 model->params.N2cc - 1指向的点会不同,逐渐完成新背景的更新 //个人认为此处改为插入排序更好,因为本来就是有序的嘛。但是数据较少(indx最大不会超过N2cc),任何排序方法当不会有太大的时间区分 for( k = 0; k < indx; k++ ) { if( PV_CC(k) <= PV_CC(indx) ) { //shift elements CvBGPixelCCStatTable tmp1, tmp2 = cctable[indx]; for( l = k; l <= indx; l++ ) { tmp1 = cctable[l]; cctable[l] = tmp2; tmp2 = tmp1; } break; } } float sum1=0, sum2=0; //check "once-off" changes //一下考虑瞬时全局变化对背景的影响,如开关电灯的影响 //具体策略见论文的 3.4.1.2 //如果取图像概率和背景概率相差过大 即可认为发生了onec-off的变化 for(k = 0; PV_CC(k) && k < model->params.N1cc; k++ ) { sum1 += PV_CC(k); sum2 += PVB_CC(k); } if( sum1 > model->params.T ) stat->is_trained_dyn_model = 1; diff = sum1 - stat->Pbcc * sum2; // Update stat table: //若发生此变化 更新此点位背景概率 if( diff > model->params.T ) { //printf("once off change at motion mode\n"); //new BG features are discovered for( k = 0; PV_CC(k) && k < model->params.N1cc; k++ ) { PVB_CC(k) = (PV_CC(k)-stat->Pbcc*PVB_CC(k))/(1-stat->Pbcc); } assert(stat->Pbcc<=1 && stat->Pbcc>=0); } } // Handle "stationary" pixel: /*一下考虑的是背景差分发生变化的情况下对点的更新 与帧间差分情况下处理的策略完全相同 仅仅是采用的特征向量不同而已 不再特别解释 */ if( !((uchar*)model->Ftd->imageData)[i*mask_step+j] ) { float alpha = stat->is_trained_st_model ? model->params.alpha2 : model->params.alpha3; float diff = 0; int dist, min_dist = 2147483647, indx = -1; //update Pb stat->Pbc *= (1.f-alpha); if( !((uchar*)model->foreground->imageData)[i*mask_step+j] ) { stat->Pbc += alpha; } //find best Vi match for( k = 0; k < model->params.N2c; k++ ) { // Exponential decay of memory PV_C(k) *= (1-alpha); PVB_C(k) *= (1-alpha); if( PV_C(k) < MIN_PV ) { PV_C(k) = 0; PVB_C(k) = 0; continue; } dist = 0; for( l = 0; l < 3; l++ ) { int val = abs( V_C(k,l) - curr_data[l] ); if( val > deltaC ) break; dist += val; } if( l == 3 && dist < min_dist ) { min_dist = dist; indx = k; } } if( indx < 0 ) {//N2th elem in the table is replaced by a new features indx = model->params.N2c - 1; PV_C(indx) = alpha; PVB_C(indx) = alpha; //udate Vt for( l = 0; l < 3; l++ ) { V_C(indx,l) = curr_data[l]; } } else {//update PV_C(indx) += alpha; if( !((uchar*)model->foreground->imageData)[i*mask_step+j] ) { PVB_C(indx) += alpha; } } //re-sort Ct table by Pv for( k = 0; k < indx; k++ ) { if( PV_C(k) <= PV_C(indx) ) { //shift elements CvBGPixelCStatTable tmp1, tmp2 = ctable[indx]; for( l = k; l <= indx; l++ ) { tmp1 = ctable[l]; ctable[l] = tmp2; tmp2 = tmp1; } break; } } // Check "once-off" changes: float sum1=0, sum2=0; for( k = 0; PV_C(k) && k < model->params.N1c; k++ ) { sum1 += PV_C(k); sum2 += PVB_C(k); } diff = sum1 - stat->Pbc * sum2; if( sum1 > model->params.T ) stat->is_trained_st_model = 1; // Update stat table: if( diff > model->params.T ) { //printf("once off change at stat mode\n"); //new BG features are discovered for( k = 0; PV_C(k) && k < model->params.N1c; k++ ) { PVB_C(k) = (PV_C(k)-stat->Pbc*PVB_C(k))/(1-stat->Pbc); } stat->Pbc = 1 - stat->Pbc; } } // if !(change detection) at pixel (i,j) // Update the reference BG image: if( !((uchar*)model->foreground->imageData)[i*mask_step+j]) { uchar* ptr = ((uchar*)model->background->imageData) + i*model->background->widthStep+j*3; if( !((uchar*)model->Ftd->imageData)[i*mask_step+j] && !((uchar*)model->Fbd->imageData)[i*mask_step+j] ) { // Apply IIR filter: for( l = 0; l < 3; l++ ) { int a = cvRound(ptr[l]*(1 - model->params.alpha1) + model->params.alpha1*curr_data[l]); ptr[l] = (uchar)a; //((uchar*)model->background->imageData)[i*model->background->widthStep+j*3+l]*=(1 - model->params.alpha1); //((uchar*)model->background->imageData)[i*model->background->widthStep+j*3+l] += model->params.alpha1*curr_data[l]; } } else { // Background change detected: for( l = 0; l < 3; l++ ) { //((uchar*)model->background->imageData)[i*model->background->widthStep+j*3+l] = curr_data[l]; ptr[l] = curr_data[l]; } } } } // j } // i // Keep previous frame: cvCopy( curr_frame, model->prev_frame ); return region_count; } /* End of file. */