基于codebook背景建模的运动目标检测

来源:互联网 发布:开淘宝网店发货图片 编辑:程序博客网 时间:2024/05/16 12:46

帧差法可以用来检测运动目标,简单的背景模型建模是以图像序列均值以及差序列均值为基础的,优点是比较简单,缺点以均值为基础的背景模型无法涵括亮度跳度较大的周期背景(例如摇曳的树、转动的风扇、摆动窗帘等等)。

codebook用来描述一个像素位置的信息,是多个编码元(一个编码本),每个编码元主要包含一个box和两个学习区域。如下图可以理解为一个像素位置的两个编码元(编码元1相比编码元2来说对应更高的灰度区域)。

编码本示意图


codebook同样需要一定背景图像序列学习,学习过程:

任意一个像素位置由一个编码本组成,一个编码本由多个编码元组成。每个编码元包含一个box,两块学习区域。当一个像素点灰度值在已有的任何一个box规定的范围内,我们直接判断其属于该编码元,跳过即可;当其不在box范围内,但在学习区域内,我们认为其属于该编码元,但需要进行box区域范围的更新(算是一种学习);若其同样不在学习区域内,则建立新的编码元。

codebook去除干扰:

每个编码元内包含一个t参数,是距离上次被查到属于该编码元时所过去的时间,用于排除不常使用的编码元。例如,可以设置一个时间阈值t=20帧,则将删除过去20帧内没有被访问到的编码元。

codebook前景检测:

与建立编码本过程类似,只是此时不再考虑学习区域,一个像素通过判断其是否属于该位置点的编码元,如果全都不属于,则认为是前景。

codebook主要的前景分割参数:

minMod[i]   = 10;   // 用于背景差分函数中maxMod[i]   = 10;   // 调整其值以达到最好的分割

其含义为每个box的波动区域,值越大,被检测的像素则更可能在box中,即背景。
因此,如果原图噪声较大,可以设置大一些,去噪噪声,但也会导致一些目标信息的丢失,因此实际使用时需要不断实验得到最好的值。


使用CODEBOOK背景模型过程:
(1)使用函数update_codebook()在几秒或几分钟的时间内训练一个基本的背景模型。
(2)调用函数clear_stale_entries()清除stale索引。
(3)调整阈值minmod和maxmod对已知前景达到最好分割。
(4)保持一个更高级的场景模型
(5)通过函数background_diff()使用训练好的模型将前景从背景中分割出来。
(6)定期更新学习的背景像素。
(7)在一个频率较慢的情况下,用函数clear_stale_entries()定期清理stale的codebook索引


以下为随机截取的几帧实验图像
女人和她的影子

男人和他的影子

两个人和影子

单人


可以看出,codebook背景模型由于更加复杂,对运动目标的检测效果还是不错的,同时训练数据速度也比较快。但它不能很好处理不同模式的光(如早晨、中午和傍晚的阳光,室内开关灯等),因为某一场景会因长时间没被访问而删除,这种全局变化的类型可以考虑用几种不同条件下的codebook模型处理。

#include <cv.h>         #include <highgui.h>#include <cxcore.h>#define CHANNELS 3      // 设置处理的图像通道数,要求小于等于图像本身的通道数///////////////////////////////////////////////////////////////////////////// 下面为码本码元的数据结构// 处理图像时每个像素对应一个码本,每个码本中可有若干个码元// 当涉及一个新领域,通常会遇到一些奇怪的名词,不要被这些名词吓坏,其实思路都是简单的typedef struct ce {    uchar   learnHigh[CHANNELS];    // High side threshold for learning    // 此码元各通道的阀值上限(学习界限)    uchar   learnLow[CHANNELS];     // Low side threshold for learning    // 此码元各通道的阀值下限    // 学习过程中如果一个新像素各通道值x[i],均有 learnLow[i]<=x[i]<=learnHigh[i],则该像素可合并于此码元    uchar   max[CHANNELS];          // High side of box boundary    // 属于此码元的像素中各通道的最大值    uchar   min[CHANNELS];          // Low side of box boundary    // 属于此码元的像素中各通道的最小值    int     t_last_update;          // This is book keeping to allow us to kill stale entries    // 此码元最后一次更新的时间,每一帧为一个单位时间,用于计算stale    int     stale;                  // max negative run (biggest period of inactivity)    // 此码元最长不更新时间,用于删除规定时间不更新的码元,精简码本} code_element;                     // 码元的数据结构typedef struct code_book {    code_element    **cb;    // 码元的二维指针,理解为指向码元指针数组的指针,使得添加码元时不需要来回复制码元,只需要简单的指针赋值即可    int             numEntries;    // 此码本中码元的数目    int             t;              // count every access    // 此码本现在的时间,一帧为一个时间单位} codeBook;                         // 码本的数据结构///////////////////////////////////////////////////////////////////////////////////// int updateCodeBook(uchar *p, codeBook &c, unsigned cbBounds)// Updates the codebook entry with a new data point//// p            Pointer to a YUV pixel// c            Codebook for this pixel// cbBounds     Learning bounds for codebook (Rule of thumb: 10)// numChannels  Number of color channels we're learning//// NOTES://      cvBounds must be of size cvBounds[numChannels]//// RETURN//  codebook indexint cvupdateCodeBook(uchar *p, codeBook &c, unsigned *cbBounds, int numChannels){    if(c.numEntries == 0) c.t = 0;    // 码本中码元为零时初始化时间为0    c.t += 1;   // Record learning event    // 每调用一次加一,即每一帧图像加一    //SET HIGH AND LOW BOUNDS    int n;    unsigned int high[3],low[3];    for (n=0; n<numChannels; n++)    {        high[n] = *(p+n) + *(cbBounds+n);        // *(p+n) 和 p[n] 结果等价,经试验*(p+n) 速度更快        if(high[n] > 255) high[n] = 255;        low[n] = *(p+n)-*(cbBounds+n);        if(low[n] < 0) low[n] = 0;        // 用p 所指像素通道数据,加减cbBonds中数值,作为此像素阀值的上下限    }    //SEE IF THIS FITS AN EXISTING CODEWORD    int matchChannel;       int i;    for (i=0; i<c.numEntries; i++)    {        // 遍历此码本每个码元,测试p像素是否满足其中之一        matchChannel = 0;        for (n=0; n<numChannels; n++)            //遍历每个通道        {            if((c.cb[i]->learnLow[n] <= *(p+n)) && (*(p+n) <= c.cb[i]->learnHigh[n])) //Found an entry for this channel            // 如果p 像素通道数据在该码元阀值上下限之间            {                   matchChannel++;            }        }        if (matchChannel == numChannels)        // If an entry was found over all channels            // 如果p 像素各通道都满足上面条件        {            c.cb[i]->t_last_update = c.t;            // 更新该码元时间为当前时间            // adjust this codeword for the first channel            for (n=0; n<numChannels; n++)                //调整该码元各通道最大最小值            {                if (c.cb[i]->max[n] < *(p+n))                    c.cb[i]->max[n] = *(p+n);                else if (c.cb[i]->min[n] > *(p+n))                    c.cb[i]->min[n] = *(p+n);            }            break;        }    }    // ENTER A NEW CODE WORD IF NEEDED    if(i == c.numEntries)  // No existing code word found, make a new one    // p 像素不满足此码本中任何一个码元,下面创建一个新码元    {        code_element **foo = new code_element* [c.numEntries+1];        // 申请c.numEntries+1 个指向码元的指针        for(int ii=0; ii<c.numEntries; ii++)            // 将前c.numEntries 个指针指向已存在的每个码元            foo[ii] = c.cb[ii];        foo[c.numEntries] = new code_element;        // 申请一个新的码元        if(c.numEntries) delete [] c.cb;        // 删除c.cb 指针数组        c.cb = foo;        // 把foo 头指针赋给c.cb        for(n=0; n<numChannels; n++)            // 更新新码元各通道数据        {            c.cb[c.numEntries]->learnHigh[n] = high[n];            c.cb[c.numEntries]->learnLow[n] = low[n];            c.cb[c.numEntries]->max[n] = *(p+n);            c.cb[c.numEntries]->min[n] = *(p+n);        }        c.cb[c.numEntries]->t_last_update = c.t;        c.cb[c.numEntries]->stale = 0;        c.numEntries += 1;    }    // OVERHEAD TO TRACK POTENTIAL STALE ENTRIES    for(int s=0; s<c.numEntries; s++)    {        // This garbage is to track which codebook entries are going stale        int negRun = c.t - c.cb[s]->t_last_update;        // 计算该码元的不更新时间        if(c.cb[s]->stale < negRun)             c.cb[s]->stale = negRun;    }    // SLOWLY ADJUST LEARNING BOUNDS    for(n=0; n<numChannels; n++)        // 如果像素通道数据在高低阀值范围内,但在码元阀值之外,则缓慢调整此码元学习界限    {        if(c.cb[i]->learnHigh[n] < high[n])             c.cb[i]->learnHigh[n] += 1;        if(c.cb[i]->learnLow[n] > low[n])             c.cb[i]->learnLow[n] -= 1;    }    return(i);}///////////////////////////////////////////////////////////////////////////////////// uchar cvbackgroundDiff(uchar *p, codeBook &c, int minMod, int maxMod)// Given a pixel and a code book, determine if the pixel is covered by the codebook//// p        pixel pointer (YUV interleaved)// c        codebook reference// numChannels  Number of channels we are testing// maxMod   Add this (possibly negative) number onto max level when code_element determining if new pixel is foreground// minMod   Subract this (possible negative) number from min level code_element when determining if pixel is foreground//// NOTES:// minMod and maxMod must have length numChannels, e.g. 3 channels => minMod[3], maxMod[3].//// Return// 0 => background, 255 => foregrounduchar cvbackgroundDiff(uchar *p, codeBook &c, int numChannels, int *minMod, int *maxMod){    // 下面步骤和背景学习中查找码元如出一辙    int matchChannel;    //SEE IF THIS FITS AN EXISTING CODEWORD    int i;    for (i=0; i<c.numEntries; i++)    {        matchChannel = 0;        for (int n=0; n<numChannels; n++)        {            if ((c.cb[i]->min[n] - minMod[n] <= *(p+n)) && (*(p+n) <= c.cb[i]->max[n] + maxMod[n]))                matchChannel++; //Found an entry for this channel            else                break;        }        if (matchChannel == numChannels)            break; //Found an entry that matched all channels    }    if(i == c.numEntries)         // p像素各通道值满足码本中其中一个码元,则返回白色        return(255);    return(0);}//UTILITES////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////int clearStaleEntries(codeBook &c)// After you've learned for some period of time, periodically call this to clear out stale codebook entries////c     Codebook to clean up//// Return// number of entries clearedint cvclearStaleEntries(codeBook &c){    int staleThresh = c.t >> 1;         // 设定刷新时间    int *keep = new int [c.numEntries]; // 申请一个标记数组    int keepCnt = 0;                    // 记录不删除码元数目    //SEE WHICH CODEBOOK ENTRIES ARE TOO STALE    for (int i=0; i<c.numEntries; i++)        // 遍历码本中每个码元    {        if (c.cb[i]->stale > staleThresh)               // 如码元中的不更新时间大于设定的刷新时间,则标记为删除            keep[i] = 0; //Mark for destruction        else        {            keep[i] = 1; //Mark to keep            keepCnt += 1;        }    }    // KEEP ONLY THE GOOD    c.t = 0;                        //Full reset on stale tracking    // 码本时间清零    code_element **foo = new code_element* [keepCnt];    // 申请大小为keepCnt 的码元指针数组    int k=0;    for(int ii=0; ii<c.numEntries; ii++)    {        if(keep[ii])        {            foo[k] = c.cb[ii];            foo[k]->stale = 0;      //We have to refresh these entries for next clearStale            foo[k]->t_last_update = 0;            k++;        }    }    //CLEAN UP    delete [] keep;    delete [] c.cb;    c.cb = foo;    // 把foo 头指针地址赋给c.cb     int numCleared = c.numEntries - keepCnt;    // 被清理的码元个数    c.numEntries = keepCnt;    // 剩余的码元地址    return(numCleared);}// 寻找掩模轮廓void find_connected_componts(IplImage *raw,IplImage *mask,int ploy_hull0 = 1,float perimScale = 4,CvRect *bbs = NULL,CvPoint *centers = NULL){    static CvMemStorage* mem_storage = NULL;    static CvSeq* contours = NULL;    //为寻找轮廓定义存储空间    /*cvMorphologyEx(mask,mask,0,0,CV_MOP_OPEN,1);     cvMorphologyEx(mask,mask,0,0,CV_MOP_CLOSE,1); */    //形态学去除噪声     if(mem_storage==NULL)     {        mem_storage = cvCreateMemStorage(0);     }       else     {        cvClearMemStorage(mem_storage);     }      CvContourScanner scanner = cvStartFindContours(mask,mem_storage,sizeof(CvContour),CV_RETR_EXTERNAL,CV_CHAIN_APPROX_SIMPLE);      //寻找外轮廓,保存在scanner中       CvSeq *c;      int numCont = 0;       while((c=cvFindNextContour(scanner))!=NULL)       {           double len = cvContourPerimeter(c);             //该轮廓的长度           double q = (mask->height+mask->width)/perimScale;           //所需要轮廓的长度,小于则删除该轮廓           if (len<q)           {               cvSubstituteContour(scanner,NULL);           }           else           {               CvSeq *c_new;               c_new = cvApproxPoly(c,sizeof(CvContour),mem_storage,CV_POLY_APPROX_DP,0,0);               //多边形近似,减少无用点(貌似还可以用凸优化近似)               cvSubstituteContour(scanner,c_new);               //替换原轮廓               numCont++;           }       }       contours = cvEndFindContours(&scanner);       CvScalar CVX_WHITE = CV_RGB(0xff,0xff,0xff);       CvScalar CVX_BLACK = CV_RGB(0x00,0x00,0x00);       //定义绘制轮廓颜色       cvZero(0);       //在新的掩模绘制       CvRect rect;       int i=0;       for(i=0,c=contours;c!=NULL&i<10;c=c->h_next,i++)       {           bbs[i] = cvBoundingRect(c);           rect = bbs[i];           cvDrawContours(mask,c,CVX_WHITE,CVX_WHITE,-1,2,8);           cvRectangle(raw,cvPoint(rect.x,rect.y),cvPoint(rect.x+rect.width,rect.y+rect.height),CVX_WHITE,1,8,0);       }       cvNamedWindow("Raw");       cvShowImage("Raw", raw);       cvNamedWindow("CodeBook");       cvShowImage("CodeBook",mask);       cvWaitKey(30);}int main(){    ///////////////////////////////////////    // 需要使用的变量    CvCapture*  capture;    IplImage*   rawImage;    IplImage*   yuvImage;    IplImage*   ImaskCodeBook;    codeBook*   cB;    CvRect      bbs[20];    unsigned    cbBounds[CHANNELS];    uchar*      pColor; //YUV pointer    int         imageLen;    int         nChannels = CHANNELS;    int         minMod[CHANNELS];    int         maxMod[CHANNELS];    //////////////////////////////////////////////////////////////////////////    // 初始化各变量    cvNamedWindow("Raw");    cvNamedWindow("CodeBook");    capture = cvCreateFileCapture("Walk2.mpg");    if (!capture)    {        printf("Couldn't open the capture!");        return -1;    }    rawImage = cvQueryFrame(capture);    yuvImage = cvCreateImage(cvGetSize(rawImage), 8, 3);        // 给yuvImage 分配一个和rawImage 尺寸相同,8位3通道图像    ImaskCodeBook = cvCreateImage(cvGetSize(rawImage), IPL_DEPTH_8U, 1);    // 为ImaskCodeBook 分配一个和rawImage 尺寸相同,8位单通道图像    cvSet(ImaskCodeBook, cvScalar(255));    // 设置单通道数组所有元素为255,即初始化为白色图像    imageLen = rawImage->width * rawImage->height;    cB = new codeBook[imageLen];    // 得到与图像像素数目长度一样的一组码本,以便对每个像素进行处理    for (int i=0; i<imageLen; i++)        // 初始化每个码元数目为0        cB[i].numEntries = 0;    for (int i=0; i<nChannels; i++)    {        cbBounds[i] = 10;   // 用于确定码元各通道的阀值        minMod[i]   = 10;   // 用于背景差分函数中        maxMod[i]   = 10;   // 调整其值以达到最好的分割    }    //////////////////////////////////////////////////////////////////////////    // 开始处理视频每一帧图像    for (int i=0;;i++)    {        cvCvtColor(rawImage, yuvImage, CV_BGR2YCrCb);        // 色彩空间转换,将rawImage 转换到YUV色彩空间,输出到yuvImage        // 即使不转换效果依然很好        // yuvImage = cvCloneImage(rawImage);        if (i <= 100)            // 100帧内进行背景学习        {            pColor = (uchar *)(yuvImage->imageData);            // 指向yuvImage 图像的通道数据            for (int c=0; c<imageLen; c++)            {                cvupdateCodeBook(pColor, cB[c], cbBounds, nChannels);                // 对每个像素,调用此函数,捕捉背景中相关变化图像                pColor += 3;                // 3 通道图像, 指向下一个像素通道数据            }            if (i == 100)                // 到100 帧时调用下面函数,删除码本中陈旧的码元            {                for (int c=0; c<imageLen; c++)                    cvclearStaleEntries(cB[c]);            }        }        else        {            uchar maskPixelCodeBook;            pColor = (uchar *)((yuvImage)->imageData); //3 channel yuv image            uchar *pMask = (uchar *)((ImaskCodeBook)->imageData); //1 channel image            // 指向ImaskCodeBook 通道数据序列的首元素            for(int c=0; c<imageLen; c++)            {                maskPixelCodeBook = cvbackgroundDiff(pColor, cB[c], nChannels, minMod, maxMod);                    *pMask++ = maskPixelCodeBook;                pColor += 3;                // pColor 指向的是3通道图像            }        }        find_connected_componts(rawImage,ImaskCodeBook,1,4.0,bbs);        if (!(rawImage = cvQueryFrame(capture)))            break;    }       cvReleaseCapture(&capture);    if (yuvImage)        cvReleaseImage(&yuvImage);    if(ImaskCodeBook)         cvReleaseImage(&ImaskCodeBook);    cvDestroyAllWindows();    delete [] cB;    return 0;}
0 0
原创粉丝点击