OpenCV-最大极值稳定区域MSER分析

最大稳定极值区域MSER是一种类似分水岭图像的分割与匹配算法,它具有仿射不变性。极值区域反映的就是集合中的像素灰度值总大于或小于其邻域区域像素的灰度值。对于最大稳定区域,通过局部阈值集操作,区域内的像素数量变化是最小的。

MSER的基本原理是对一幅灰度图像(灰度值为0~255)取阈值进行二值化处理,阈值从0到255依次递增。阈值的递增类似于分水岭算法中的水面的上升,随着水面的上升,有一些较矮的丘陵会被淹没,如果从天空往下看,则大地分为陆地和水域两个部分,这类似于二值图像。在得到的所有二值图像中,图像中的某些连通区域变化很小,甚至没有变化,则该区域就被称为最大稳定极值区域。这类似于当水面持续上升的时候,有些被水淹没的地方的面积没有变化。

上述做法只能检测出灰度图像的黑色区域,不能检测出白色区域,因此还需要对原图进行反转,然后再进行阈值从0~255的二值化处理过程。这两种操作又分别称为MSER+和MSER-。

MSER是当前认为性能最好的仿射不变性区域的检测方法,其使用不同灰度阈值对图像进行二值化来得到最稳定区域,表现特征有以下三点:对图像灰度仿射变化具有不变性,对区域支持相对灰度变化具有稳定性,对区域不同精细成都的大小区域都能进行检测。

MSER最大极值稳定区域的提取步骤:1.像素点排序2.极值区域生成3.稳定区域判定4.区域你和5.区域归一化


opencv里并没有提取出树的信息,所以先依照opencv的代码介绍ER。ER代表着是图片中一个连通(比如4连通或8连通)区域的集合,此集合内所有的像素值都小于等于某一值,而这个区域内的边界都大于这个值。我们可以把像素的值想象成地势,而把一个ER想象成一个填满水的坑洼的水坑(在这里我们采用4连通)。在这个水坑里,有一个水位淹没了所里面所有的像素但,也就是说这个区域里所有的地势(像素值)都要低于这个水位,并且水也流不出去,因为水盆有个边缘(边缘像素值要高于这个水位)。虽然水流的方式跟现实中有些区别,但是大体意思是一致的,后面会提到。

考虑如下一个简单的3 * 3的一个图片

3

2

2

2

3

1

1

2

3

的提取方式如下图,为了方便讲解,在每个操作上都打了ID(上方的红色数字),参考流程图和代码,详细过程和流程如下:


注意一般在最开始会放一个水位最高的256的dummy component作为根节点,因为图像的最高值在255。另外开始点从(1, 0)开始(坐标行在前,列在后),可以稍微节省点时间。边界存储的是与当前ER连接的边界坐标,也就是水盆边界的位置。GrowHistory存的是一个ER从低水位到高水位的过程,所有的ER(除了全图)都会存于这个history中,opencv中history中代表parent的是shortcut, 这在计算MSER的时候就不应是父节点,但在我们这里是一样的,history中parent child变量跟MSER中是不一致的,不然opencv的代码就已经提取出树的信息。
(1)执行'1' ' 2' ' 3',红色位置代表当前像素,如果某个位置被黄色填充,代表这个像素已经被访问。这部分主要是些初始化的工作。也就是在(1, 0)的像素点上放充分量的水,水位的值也就等于当前的像素值2.


(2)现在有水停留在红色位置(1, 0),并且水位为2。水每次只流向一个方向,而不能同时扩散。跟opencv的代码保持一致采用右下左右的顺序。首先执行'7'->'8'->'10', 水尝试往右流到(1,1),发现那里的地势为3,比当前的水位要高,自然流不过去,因此应该是个边缘,所以把(1, 1)加入到地势为3的边缘中。同理执行'7'->'8'->'4'->'3'现在水尝试往下流,发现坐标为(2, 0)地势为1的像素。很显然我们的水位可以流向那,这时水位降低为1,先增一个ER区,而地势为2的(1, 0)成了边界。


(3)现在在(2, 0),水位为1, 。执行'7'->'8'->'10', 水尝试流向地势为2的(2, 1),流不通, 将坐标(2, 1)压入边界中。

(4)执行'7'->'6'->'5', 这时发现(2, 0)处的周围全都尝试流通过了,确认当前的像素是属于当前的ER,因此将此像素压入ER栈顶的点集中。并且找到地势最低的边界点,作为当前点。

(5)执行'9'->'12'->'13'。刚刚的水位是1,没道理说现在就流到2了。刚才的水位是1,然后发现边界的最低的地势为2,说明已经找到了一个ER,在这个区域已经没有邻域的地势小于等于1,并且边界都大于1。因此现在就是要提高水位。而且根据ER的定义,高地势的区域会包含连通的低地势区域,因此要将其合并。为了方便,grow history的ID从10开始

(6)执行'14'->'7'->'8'->'10', 将(2, 2)压入边界。执行'7'->'6'->'5', 发现当前位置已经都访问过了,将该点压入栈顶的er,因此弹出边界(1, 0),发现边界的地势跟当前的水位是一样的,因此直接将其作为该当前点。

(7)执行'7'->'8'->'10', 继续探索,还有未访问邻域(0, 0)压入边界。

(8)所有的邻域都已访问,执行'7'->'6'->'5',将当前的点压入ER栈顶,并弹出边界(0, 0)

(9)将当期的er保存的history, 并且找到地势最低的边界点,作为当前点。而且可以检查地势为1的ER是否为MSER了,依旧是Grow History ID 10保存的内容。

(10)执行'7'->'8'->'4"->'3',访问到地势为2的(0,1),因此水位再次下降

(11)继续往外探索,执行'7'->'8'->'10',将(0, 2)压入边界

(12)执行'7'->'6'->'5'->'9', 没有未访问的邻域点,将(0, 1)压入ER栈,并弹出边界,发现发现当前的像素还在一个水位上,因此不需要合并或者升水位

(13)继续探索,发现低地势的(1, 2),水位下降,将当前点压入边界

(14)现在所有的点都已访问了,将坐标(1, 2)压入ER栈,并弹出边界

(15)上一步中的边界水位比我们的要高,并观察ER栈的gray level, 因此现合并栈顶的两个ER

(16)与上面的情况类似,压入当前点到ER栈,合并栈顶量ER,并弹出边界

(17)按照之前的过程,连续压入对角线上的3,已经没有边界了,推出。自此我们找出了所有的ER


MSER Tree

按照上面的流程,我们提取了所有的ER,他们的ID分别为1,10,11,12,13.要构建树,需要定义父子关系,我们把合并过程中高地势的为父,低水位的为子,因此构建树如下:

那怎么判断一个ER是不是MSER呢?对于单通道图像来说主要有五个参数:delta, maxVariation, minDiversity, minArea, maxArea。其中minArea, maxArea代表区域的面积,如ID11的面积是3,ID10的面积是10。

而delta是为了计算variation。MSER的核心思想是要找到一块区域,能跟周围的有明显的变化。在MSER里,这个是通过variation定义的。打个比方,一个脸盆和一个水桶,脸盆底部是个ER,水桶的底部也是一个ER。但是脸盆的底部跟边缘的高度相差不大,我只要把水位增加一点,水就溢出来,脸盆的边缘和底部合成了一个新的ER。但如果是水桶,你需要加很多水才能行成新的ER。因此水桶的ER更稳定,它跟周围的对比度更强。一个定义是:

其中S代表的ER的面积,在Opencv中简化为:

比如delta = 2, 要计算ID1的vatiation, 可以看出S(ERlevel) = 9, ID1的gray level是3,因此要找到3 - 2 = 1 gray level的ER, 我们去点数最多的,都是1,因此按照上面的公式是(9 - 1) / 1,variation是8。还有个限制是当前ER的variation要小于父和子的variation。

minDiversity是为了解决两个MSER靠的很近的问题。公式如下,MSERson代表的是子节点最近的已经确认是MSER的区域。如果有个子MSER,而且两个点数比较接近,我们认为两个ER相隔太近,父的ER就不能当成MSER。

另外,如字有黑底白字和白底黑字,要把原来的图像像素反转一下img = 255 - img,按照流程再算一遍。


附opencv这部分的核心代码

static void extractMSER_8UC1_Pass( int* ioptr,  
              int* imgptr,  
              int*** heap_cur,  
              LinkedPoint* ptsptr,  
              MSERGrowHistory* histptr,  
              MSERConnectedComp* comptr,  
              int step,  
              int stepmask,  
              int stepgap,  
              MSERParams params,  
              int color,  
              CvSeq* contours,  
              CvMemStorage* storage )  
{  
    //设置第一个组块的灰度值为256,该灰度值是真实图像中不存在的灰度值,以区分真实图像的组块,从而判断程序是否结束  
    comptr->grey_level = 256;
    //指向第二个组块  
    comptr++;  
    //设置第二个组块为输入图像第一个像素(左上角)的灰度值  
    comptr->grey_level = (*imgptr)&0xff;  
    //初始化该组块  
    initMSERComp( comptr );  
    //在最高位标注该像素为已被访问过,即该值小于0  
    *imgptr |= 0x80000000;  
    //得到该像素所对应的堆,即指向它所对应的灰度值  
    heap_cur += (*imgptr)&0xff;  
    //定义方向,即偏移量,因为是4邻域,所以该数组分别对应右、下、左、上  
    int dir[] = { 1, step, -1, -step };  
#ifdef __INTRIN_ENABLED__  
    unsigned long heapbit[] = { 0, 0, 0, 0, 0, 0, 0, 0 };  
    unsigned long* bit_cur = heapbit+(((*imgptr)&0x700)>>8);  
#endif  
    //死循环,退出该死循环的条件有两个:一是到达组块的栈底;二是边界像素堆中没有任何值。达到栈底也就意味着堆中没有值,在此函数中两者是一致的。  
    for ( ; ; )  
    {  
        //在4邻域内进行搜索  
        while ( ((*imgptr)&0x70000) < 0x40000 )  
        {  
            // get the neighbor  
            /* ((*imgptr)&0x70000)>>16得到第16位至第18位数据,该数据对应的4邻域的方向,再通过dir数组得到4邻域的偏移量,因此imgptr_nbr为当前像素4邻域中某一个方向上邻域的地址指针 */  
            int* imgptr_nbr = imgptr+dir[((*imgptr)&0x70000)>>16];  
            //检查邻域像素是否被访问过,如果被访问过,则会在第一位置1,因此该值会小于0,否则第一位为0,该值大于0  
            if ( *imgptr_nbr >= 0 ) // if the neighbor is not visited yet  
            {  
                //标注该像素已被访问过,即把第一位置1  
                *imgptr_nbr |= 0x80000000; // mark it as visited  
                //比较当前像素与邻域像素灰度值  
                if ( ((*imgptr_nbr)&0xff) < ((*imgptr)&0xff) )  
                {  
                    //如果邻域值小于当前值,把当前值放入堆中   
                    //堆中该像素灰度值的数量加1,即对该灰度值像素个数计数  
                    (*heap_cur)++;  
                    //把当前值的地址放入堆中  
                    **heap_cur = imgptr;  
                    //重新标注当前值的方向位,以备下一次访问该值时搜索下一个邻域  
                    *imgptr += 0x10000;  
                    //定位邻域值所对应的堆的位置  
                    //当前heap_cur所指向的灰度值为while循环搜索中的最小灰度值,即水溢过的最低点  
                    heap_cur += ((*imgptr_nbr)&0xff)-((*imgptr)&0xff);  
#ifdef __INTRIN_ENABLED__  
                    _bitset( bit_cur, (*imgptr)&0x1f );  
                    bit_cur += (((*imgptr_nbr)&0x700)-((*imgptr)&0x700))>>8;  
#endif  
                    imgptr = imgptr_nbr;    //邻域值换为当前值  
                    comptr++;    //创建一个组块  
                    initMSERComp( comptr );    //初始化该组块  
                    comptr->grey_level = (*imgptr)&0xff;    //为该组块的灰度值赋值  
                    //当某个邻域值小于当前值,则不对当前值再做任何操作,继续下次循环,在下次循环中,处理的则是该邻域值,即再次执行步骤4  
                    continue;  
                } else {  
                    //如果邻域值大于当前值,把邻域值放入堆中  
                    // otherwise, push the neighbor to boundary heap  
                    //找到该邻域值在堆中的灰度值位置,并对其计数,即对该灰度值像素个数计数   
                    heap_cur[((*imgptr_nbr)&0xff)-((*imgptr)&0xff)]++;  
                    //把该邻域像素地址放入堆中  
                    *heap_cur[((*imgptr_nbr)&0xff)-((*imgptr)&0xff)] = imgptr_nbr;  
#ifdef __INTRIN_ENABLED__  
                    _bitset( bit_cur+((((*imgptr_nbr)&0x700)-((*imgptr)&0x700))>>8), (*imgptr_nbr)&0x1f );  
#endif  
                }  
            }  
            *imgptr += 0x10000;    //重新标注当前值的领域方向  
        }  
        //imsk表示结束while循环后所得到的最后像素地址与图像首地址的相对距离  
        int imsk = (int)(imgptr-ioptr);  
        //得到结束while循环后的最后像素的坐标位置  
        //从这里可以看出图像的宽采样2^N的好处,即imsk>>stepgap  
        ptsptr->pt = cvPoint( imsk&stepmask, imsk>>stepgap );  
        //对栈顶的组块的像素个数累加,即计算组块的面积大小,并链接组块内的像素点  
        //结束while循环后,栈顶组块的灰度值就是该次循环后得到的最小灰度值,也就是该组块为极低点,就相当于水已经流到了最低的位置  
        accumulateMSERComp( comptr, ptsptr );  
        //指向下一个像素点链表位置  
        ptsptr++;  
        /*结束while循环后,如果**heap_cur有值的话,heap_cur指向的应该是while循环中得到的灰度值最小值,也就是在组块的边界像素中,有与组块相同的灰度值,因此要把该值作为当前值继续while循环,也就是相当于组块面积的扩展*/  
        if ( **heap_cur )    //有值  
        {  
            imgptr = **heap_cur;    //把该像素点作为当前值  
            (*heap_cur)--;    //像素的个数要相应的减1  
#ifdef __INTRIN_ENABLED__  
            if ( !**heap_cur )  
                _bitreset( bit_cur, (*imgptr)&0x1f );  
#endif  
        //已经找到了最小灰度值的组块,并且边界像素堆中的灰度值都比组块的灰度值大,则这时需要组块,即计算最大稳定极值区域  
        } else {  
#ifdef __INTRIN_ENABLED__  
            bool found_pixel = 0;  
            unsigned long pixel_val;  
            for ( int i = ((*imgptr)&0x700)>>8; i < 8; i++ )  
            {  
                if ( _BitScanForward( &pixel_val, *bit_cur ) )  
                {  
                    found_pixel = 1;  
                    pixel_val += i<<5;  
                    heap_cur += pixel_val-((*imgptr)&0xff);  
                    break;  
                }  
                bit_cur++;  
            }  
            if ( found_pixel )  
#else  
            heap_cur++;    //指向高一级的灰度值  
            unsigned long pixel_val = 0;  
            //在边界像素堆中,找到边界像素中的最小灰度值  
            for ( unsigned long i = ((*imgptr)&0xff)+1; i < 256; i++ )  
            {  
                if ( **heap_cur )  
                {  
                    pixel_val = i;    //灰度值  
                    break;  
                }  
                //定位在堆中所对应的灰度值,与pixel_val是相等的  
                heap_cur++;   
            }  
            if ( pixel_val )    //如果找到了像素值  
#endif  
            {  
                imgptr = **heap_cur;    //从堆中提取出该像素  
                (*heap_cur)--;    //对应的像素个数减1  
#ifdef __INTRIN_ENABLED__  
                if ( !**heap_cur )  
                    _bitreset( bit_cur, pixel_val&0x1f );  
#endif  
                //进入处理栈子模块  
                if ( pixel_val < comptr[-1].grey_level )  
                //如果从堆中提取出的最小灰度值小于距栈顶第二个组块的灰度值,则说明栈顶组块和第二个组块之间仍然有没有处理过的组块,因此在计算完MSER值后还要继续返回步骤4搜索该组块  
                {   
                    if ( MSERStableCheck( comptr, params ) )    //是MSER  
                    {  
                        //得到组块内的像素点  
                        CvContour* contour = MSERToContour( comptr, storage );  
                        contour->color = color;    //标注是MSER-还是MSER+  
                        //把组块像素点放入序列中  
                        cvSeqPush( contours, &contour );   
                    }  
                    MSERNewHistory( comptr, histptr );  
                    //改变栈顶组块的灰度值,这样就可以和上一层的组块进行合并  
                    comptr[0].grey_level = pixel_val;  
                    histptr++;  
                } else {  
                    //从堆中提取出的最小灰度值大于等于距栈顶第二个组块的灰度值  
                    //死循环,用于处理灰度值相同并且相连的组块之间的合并  
                    for ( ; ; )  
                    {  
                        //指向距栈顶第二个组块  
                        comptr--;  
                        //合并前两个组块,并把合并后的组块作为栈顶组块  
                        MSERMergeComp( comptr+1, comptr, comptr, histptr );  
                        histptr++;  
                        /*如果pixel_val = comptr[0].grey_level,说明在边界上还有属于该组块的像素;如果pixel_val < comptr[0].grey_level,说明还有比栈顶组块灰度值更小的组块没有搜索到。这两种情况都需要回到步骤4中继续搜索组块*/  
                        if ( pixel_val <= comptr[0].grey_level )  break;  
                        //合并栈内前两个组块,直到pixel_val < comptr[-1].grey_level为止  
                        if ( pixel_val < comptr[-1].grey_level )  
                        {  
                            if ( MSERStableCheck( comptr, params ) )  
                            {  
                                CvContour* contour = MSERToContour( comptr, storage );  
                                contour->color = color;  
                                cvSeqPush( contours, &contour );  
                            }  
                            MSERNewHistory( comptr, histptr );  
                            comptr[0].grey_level = pixel_val;  
                            histptr++;  
                            break;  
                        }  
                    }  
                }  
            } else  break;//边界像素堆中没有任何像素,则退出死循环,该函数返回。
        }  
    }  
}  

你可能感兴趣的:(----opencv学习,----心得)