MSER最稳定极值区域源码分析概要.docx
《MSER最稳定极值区域源码分析概要.docx》由会员分享,可在线阅读,更多相关《MSER最稳定极值区域源码分析概要.docx(32页珍藏版)》请在冰豆网上搜索。
MSER最稳定极值区域源码分析概要
MSER最稳定极值区域源码分析
最稳定极值区域介绍
如把灰度图看成高低起伏的地形图,其中灰度值看成海平面高度的话,MSER的作用就是在灰度图中找到符合条件的坑洼。
条件为坑的最小高度,坑的大小,坑的倾斜程度,坑中如果已有小坑时大坑与小坑的变化率。
上图展示了几种不同的坑洼,根据最小高度,大小,倾斜程度这些条件的不同,选择的坑也就不同。
上图展示了最后一个条件,大坑套小坑的情况。
根据条件的不同,选择也不同。
以上便是对坑的举例,MSER主要流程就三部分组成:
1.预处理数据
2.遍历灰度图
3.判断一个区域(坑洼)是否满足条件
简单来说,就如将水注入这个地形中。
水遇到低处就往低处流,如果没有低处了,水位就会一点点增长,直至淹没整个地形。
在之前预处理下数据,在水位提高时判断下是否满足条件。
预处理数据
先说下流程中的主要部件,如下:
1.img,由原8位单通道灰度图转化的更容易遍历和记录数据的32位单通道图。
预处理内容为:
32位值记录从这点是否探索过,探索过的方向,灰度值;图大小也扩大了,最外添加了一个像素的完整一圈,值为-1可看作墙,宽度也改变为2的整数次方,用于加快运算。
如果由掩码图,如下:
2.heap,记录坑洼边界的堆栈,每个灰度值都有自己的堆栈。
预处理内容为:
计算所有灰度值的个数,这样提前就可以分配堆栈大小。
例如知道了灰度2的像素由4个,就可以将灰度2的堆栈大小分配为5(多一个位标志位空)。
p,记录水坑数据的堆栈,有水位值(灰度值),面积(像素个数和像素位置)等。
预处理内容为:
仅仅是分配内存,分配257个(0-255外多一个用作结束)
4.history,记录水位抬高的历史,就是一个小坑抬高水位后一点点变成大坑的历史。
预处理内容为:
仅仅是分配内存,大小为像素点个数(就是宽*高)。
可以想成所有点都不同都可以形成历史的最大个数。
遍历灰度图
在重复下整个简单的过程:
就如将水注入这个地形中。
水遇到低处就往低处流,如果没有低处了,水位就会一点点增长,直至淹没整个地形。
先说下主要部件:
1.img,由原8位单通道灰度图转化的更容易遍历和记录数据的32位单通道图。
遍历时:
当前像素位置中有3位记录方向(除了东南西北还有一个用来代表结束),逐个改变方向遍历。
还有最高1位记录是否发现过了。
根据方向遍历相邻像素,如果4个方向都探索过了,就从heap边界中找到一个最小灰度的边界,出栈来用作当前像素。
最终将所有像素的4个方向都走完,也是所有像素都被发现了,遍历就结束。
2.heap,记录坑洼边界的堆栈,每个灰度值都有自己的堆栈。
遍历时:
当水遇到低处时入栈当前位置为低处的边界,当水遇到相等高度或高处时入栈那个边界;当抬高水位时出栈被抬高到的边界。
p,记录水坑数据的堆栈,有水位值(灰度值),面积(像素个数和像素位置)等。
遍历时:
当水位下降时新入栈,水位提高时出栈并可能与之前的合并。
4.history,记录水位抬高的历史,就是一个小坑抬高水位后一点点变成大坑的历史。
遍历时:
history主要是记录用来判断最大稳定极值区域的数据,没有遍历的作用。
主要记录时刻有两种:
提高水位到边界heap中的最小高度,提高水位到comp中上一项的高度。
要记录灰度值,像素数,快捷指针,孩子指针,稳定时的像素数。
下面举例子,走下遍历的流程(并不是依次就是一步,一些步骤合并了)(红色为有变动位置,时间匆忙没有仔细校准每个位置):
中上,要遍历的灰度图。
为了方便观看,上文提到周围一圈的-1被去掉了。
左下,history是抬高水位的历史。
中下,comp是水位数据。
预先入栈一个256的灰度作为顶,用来抬高水位时判断边界值小还是上一个水位数据的灰度值小。
右下,heap是边界堆栈,heap_start是每个灰度指向heap堆栈的指针。
特殊说明下,heap是一个个堆栈连接在一起的一个数组,由于上面说的预处理过了,已经知道每个灰度的像素个数,所以提前指定了heap_start中每个灰度指向heap中的位置,指向0代表所在堆栈没有数据。
例如灰度2有4个像素,所以灰度3的指针从灰度2指针后5个后开始,4个是像素数,1个是代表空的0。
从A1位置开始,comp中入栈个灰度2的数据项,并将heap_cur当前指针设置为2灰度的指针。
探索A1右边B1,标识为已发现。
B1的值2没有小于当前水位值2,作为边界入栈。
探索A1下面的A2。
值1小于当前水位2,将2入栈边界栈,入栈水位数据1,调整边界指针heap_cur为指向1的指针,当前像素为A2。
探索A2右边B3与下边A3,都没有比当前水位1小,分别入栈所属灰度的边界栈。
A2所有方向都探索完,将A2加入当前水位数据comp中。
在边界栈中找到最小灰度的一个值出栈(图5里边界里有灰度2的和灰度3的,从当前灰度1开始一点点加大所以找到了灰度2),出栈了A3。
A3的灰度2,所以抬高水位。
记录历史histroy,修改当前水位数据灰度为2,边界指针heap_cur指向2灰度的堆栈。
探索A3周边,发现B3,灰度3比当前大作为边界入栈。
A3所有方向也都探索完,将A3加入当前水位数据comp中。
边界中找到A1。
由于A1灰度还是2,没有提升水位。
将A1作为当前像素。
刚刚的A1周围也早就探索完了,将A1加入当前水位数据comp中。
又在边界中找到了B1,并出栈作为当前像素。
B1右边探索到了C1,加入灰度3的边界栈。
这时,B1周围已经探索完毕,将B1加入当前水位数据comp中。
B1被加入在边界栈中从灰度2开始查找,找到灰度3中C1作为当前像素。
然后记录历史history,提高当前水位数据comp的灰度值,设置heap_cur指针到灰度3的边界栈。
从当前像素C1向下找到C2,C2灰度比当前低。
将当前像素C1入栈边界栈,新建灰度2的水位数据comp,边界指针heap_cur指向灰度2,设置C2为当前指针。
探索C2下面最后一个像素C3,将C3加入边界栈。
将C2加入水位数据comp中。
需要抬高水位了,从灰度3的边界栈中出栈C3,发现灰度和上一个水位数据comp的灰度一样,需要合并这两个comp数据。
添加历史history,合并两个comp数,改变边界栈heap_cur到灰度3,设置C3为当前像素。
最后的C3,C1,B3,B2周围都没有可以探索的像素了,依次出栈加入水位数据。
至此所有9个像素都探索完毕了。
判断一个区域(坑洼)是否满足条件
先看下参数:
int delta; //两个区域间的灰度差
int minArea; //区域最小像素数
int maxArea; //区域最大像素数
double maxVariation; //两个区域的偏差
double minDiversity; //当前区域与稳定区域的变化率
一个水坑的变化如下图A,随着水位的提高,面积由Ra变为Rb在到Rc,Ra为Rb的父区域;判断极值区域的方法如图B,在delta水位差间两个区域面积是否满足一定条件;还有一个判断条件如图C,如果已经有一个候选区域Rstable了,Rcandidate是否可以作为一个极值区域,也就是大坑套小坑的情况。
maxVariation是上图B的情况,值为下面的公式A;minDiversity是上图C的情况,值为下面公式B:
下面是在条件判断时两个有用的部件(其他没有任何作用):
p,记录水坑数据的堆栈,有水位值(灰度值),面积(像素个数和像素位置)等。
条件判断时:
有个history指向当前区域的历史的指针,用来查找当前区域之前的变化历史;var用来记录上次计算的variation;div用来记录上次计算的diversity。
(var与div用来确保坑越来越稳定,如果与上次的值比较发散了则不满足条件)
4.history,记录水位抬高的历史,就是一个小坑抬高水位后一点点变成大坑的历史。
条件判断时:
每一个历史项都有指向孩子历史的指针child,与指向相差delta灰度历史的快捷指针shortcut,还有上次稳定时的像素数stable,最后就是那个历史时刻的灰度值val与像素数size。
(快捷指针是用来加速计算的,在历史里一个一个向前找也能找到,但总没有直接在上次找到的位置前后找更快吧:
))
源码
基本结构:
typedefstructLinkedPoint
{
structLinkedPoint*prev;
structLinkedPoint*next;
Pointpt;
}
LinkedPoint;
//thehistoryofregiongrown
typedefstructMSERGrowHistory
{
//快捷路径,是指向以前历史的指针。
因为不是一个一个连接的,所以不是parent。
算法中是记录灰度差为delta的历史的指针。
//例如:
当前是灰度是10,delta=3,这个指针就指向灰度为7时候的历史
structMSERGrowHistory*shortcut;
//指向更新历史的指针,就是从这个历史繁衍的新历史,所以叫孩子
structMSERGrowHistory*child;
//大于零代表稳定,值是稳定是的像素数。
这个值在不停的继承
intstable;//wheniteverstabledbefore,recordthesize
//灰度值
intval;
//像素数
intsize;
}
MSERGrowHistory;
typedefstructMSERConnectedComp
{
//像素点链的头
LinkedPoint*head;
//像素点链的尾
LinkedPoint*tail;
//区域上次的增长历史,可以通过找个历史找到之前的记录
MSERGrowHistory*history;
//灰度值
unsignedlonggrey_level;
//像素数
intsize;
intdvar;//thederivativeoflastvar
floatvar;//thecurrentvariation(mosttimeisthevariationofone-stepback)}
MSERConnectedComp;
structMSERParams
{
MSERParams(int_delta,int_minArea,int_maxArea,double_maxVariation,
double_minDiversity,int_maxEvolution,double_areaThreshold,
double_minMargin,int_edgeBlurSize)
:
delta(_delta),minArea(_minArea),maxArea(_maxArea),maxVariation(_maxVariation),
minDiversity(_minDiversity),maxEvolution(_maxEvolution),areaThreshold(_areaThreshold),
minMargin(_minMargin),edgeBlurSize(_edgeBlurSize)
{}
//MSER使用
intdelta;//两个区域间的灰度差
intminArea;//区域最小像素数
intmaxArea;//区域最大像素数
doublemaxVariation;//两个区域的偏差
doubleminDiversity;//当前区域与稳定区域的变化率
//MSCR使用
intmaxEvolution;
doubleareaThreshold;
doubleminMargin;
intedgeBlurSize;
};
预处理:
//topreprocesssrcimagetofollowingformat
//32-bitimage
//>0isavailable,<0isvisited
//17~19bitsisthedirection
//8~11bitsisthebucketitfallsto(forBitScanForward)
//0~8bitsisthecolor
/**@brief将所给原单通道灰度图和掩码图预处理为一张方便遍历与记录数据的32位单通道图像;并且根据像素灰度值分配边缘栈。
*32位格式如下:
*>0可用,<0已经被访问
*17~19位用于记录下一个要探索的方向,5个值
*8~11位用于优化的二值搜索
*0~8位用于记录灰度值
*@paramheap_cur边缘栈
*@paramsrc原单通道灰度图
*@parammask掩码图
*/
staticint*preprocessMSER_8UC1(CvMat*img,
int***heap_cur,
CvMat*src,
CvMat*mask)
{
//数据有效内容是在img中,由一圈-1包围着,靠左的区域。
也就是被一圈-1的墙包围着。
//原始数据跳转到下一行的偏移量。
intsrccpt=src->step-src->cols;
//跳转到下一行的偏移量,最后减一是因为,例如:
xoooxxx,o是有效数据,x是扩充出来的。
偏移量应该是3,就是ooo最
//右边的xxx个数。
为了计算,就需要减去ooo最左面的一个x。
intcpt_1=img->cols-src->cols-1;
int*imgptr=img->data.i;
int*startptr;
//用于记录每个灰度有多少像素
intlevel_size[256];
for(inti=0;i<256;i++)
level_size[i]=0;
//设置第一行为-1
for(inti=0;icols+2;i++)
{
*imgptr=-1;
imgptr++;
}
//偏移到第一个有效数据所在行的开头
imgptr+=cpt_1-1;
uchar*srcptr=src->data.ptr;
if(mask)
{
//有掩码
startptr=0;//数据处理的开始位置,为最左上的位置。
uchar*maskptr=mask->data.ptr;
for(inti=0;irows;i++)
{
//最左面设置为-1
*imgptr=-1;
imgptr++;
for(intj=0;jcols;j++)
{
if(*maskptr)
{
if(!
startptr)
startptr=imgptr;
//灰度值取反!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
*srcptr=0xff-*srcptr;
//所在灰度值个数自增
level_size[*srcptr]++;
//写入0~8位,8~13位用作BitScanForward
*imgptr=((*srcptr>>5)<<8)|(*srcptr);
}
else{
//标为-1,就是当作一个已经被发现的位置,和外围-1墙的原理一样
*imgptr=-1;
}
imgptr++;
srcptr++;
maskptr++;
}
//最右面设置为-1
*imgptr=-1;
//都跳到下一行开始
imgptr+=cpt_1;
srcptr+=srccpt;
maskptr+=srccpt;
}
}
else{
//就是没有掩码的情况
startptr=imgptr+img->cols+1;
for(inti=0;irows;i++)
{
*imgptr=-1;
imgptr++;
for(intj=0;jcols;j++)
{
*srcptr=0xff-*srcptr;
level_size[*srcptr]++;
*imgptr=((*srcptr>>5)<<8)|(*srcptr);
imgptr++;
srcptr++;
}
*imgptr=-1;
imgptr+=cpt_1;
srcptr+=srccpt;
}
}
//设置最后一行为-1
for(inti=0;icols+2;i++)
{
*imgptr=-1;
imgptr++;
}
//确定每个灰度在边界堆中的指针位置。
0代表没有值。
heap_cur[0][0]=0;
for(inti=1;i<256;i++)
{
heap_cur[i]=heap_cur[i-1]+level_size[i-1]+1;
heap_cur[i][0]=0;
}
returnstartptr;
}
主流程及遍历方法:
staticvoidextractMSER_8UC1_Pass(int*ioptr,
int*imgptr,
int***heap_cur,//边界栈的堆,里面是每一个灰度的栈
LinkedPoint*ptsptr,
MSERGrowHistory*histptr,
MSERConnectedComp*comptr,
intstep,
intstepmask,
intstepgap,
MSERParamsparams,
intcolor,
CvSeq*contours,
CvMemStorage*storage)
{
//ER栈第一项为结束的标识项,值为大于255的256
comptr->grey_level=256;
//将当前位置值入栈,并初始化
comptr++;
comptr->grey_level=(*imgptr)&0xff;
initMSERComp(comptr);
//设置为已经发现
*imgptr|=0x80000000;
//加上灰度偏移就将指针定位到了相应灰度的边界栈上
heap_cur+=(*imgptr)&0xff;
//四个方向的偏移量,上下的偏移是隔行的步长
intdir[]={1,step,-1,-step};
#ifdef__INTRIN_ENABLED__
unsignedlongheapbit[]={0,0,0,0,0,0,0,0};
unsignedlong*bit_cur=heapbit+(((*imgptr)&0x700)>>8);#endif
//循环
for(;;)
{
//taketourofallthe4directions
//提取当前像素的方向值,判断是否还有方向没有走过
while(((*imgptr)&0x70000)<0x40000)
{
//gettheneighbor
//通过方向对应的偏移获得相邻像素指针
int*imgptr_nbr=imgptr+dir[((*imgptr)&0x70000)>>16];
//判断是否访问过
if(*imgptr_nbr>=0)//iftheneighborisnotvisitedyet{
//没有访问过,标记为访问过
*imgptr_nbr|=0x80000000;//markitasvisited
if(((*imgptr_nbr)&0xff)<((*imgptr)&0xff))
{
//whenthevalueofneighborsmallerthancurrent
//pushcurrenttoboundaryheapandmaketheneighbortobethecurrentone
//createanemptycomp
//如果相邻像素的灰度小于当前像素,将当前像素加入边界栈堆,并把相邻像素设置为当前像素,并新建ER栈项
//将当前加入边界栈堆
(*heap_cur)++;
**heap_cur=imgptr;
//转换方向
*imgptr+=0x10000;
//将边界栈堆的指针调整为相邻的像素灰度所对应的位置
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;
//新建ER栈项,并设置灰度为当前像素灰度
comptr++;
initMSERComp(comptr);
comptr->grey_level=(*imgptr)&0xff;
continue;
}
else{
//otherwise,pushtheneighbortoboundaryheap
//否则,将相邻像素添加到对应的边界帧堆中
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)&0