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

article/2025/9/13 5:59:07

最大稳定极值区域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;//边界像素堆中没有任何像素,则退出死循环,该函数返回。}  }  
}  

http://chatgpt.dhexx.cn/article/Pf3UGStp.shtml

相关文章

【转载】项目实战—文档区域MSER检测实战(十)

上次已经讨论过相关的理论&#xff0c;这次我们来进行相关的实战。 OCR相关工作都有一个第一步&#xff0c;那就是检测图像中的文本区域&#xff0c;只有找到了文本区域&#xff0c;才能对其内容进行识别&#xff0c;也只有找到了文本区域&#xff0c;才能更有针对性地判断该文…

opencv 中将 MSER 修改成 Hierarchical MSER 方法

http://code.opencv.org/issues/1577 描述 I attach a patch implementing the Hierarchical MSER. It extends the OpenCV MSER implementation to return the underlying component tree used by the MSER algorithm. The tree is returned in the CvSeq tree fields (h_next…

OpenCV实践之MSER/MSCR极值区域检测算法

MSER/MSCR极值区域检测算法 OpenCV中features2d.hpp中MSER类接口实现了MSER极值区域检测算法&#xff0c;MSER类根据输入参数判断是否为彩色or灰度图像进行不同的算法检测。若输入为灰度图像&#xff0c;那么采取MSER极值区域检测算法&#xff0c;若输入为彩色图像&#xff0c;…

MSER仿射不变特征匹配算法

MSER原理简述 个人博客 OpenCV实践之MSER仿射匹配算法 已更新讲述MSER仿射匹配算法代码      区域检测(Region Detection)方法是根据图像中具有某种同类性质的像元进行分类(例如相同像素值大小的点),然后把具有相同性质的像元合并成区域,实现区域的检测即图像分割。MSER…

OpenCV用MSER 算法提取特征区域

计算图像 MSER 的基础类是 cv::MSER&#xff0c;继承自 cv::Feature2D 类&#xff0c;cv::MSER 类的实例可以通过create 方法创建。我们在初始化时指定被检测区域的最小和最大尺寸&#xff0c;以便限制被检测特征的数量&#xff0c;调用方式如下&#xff1a; cv::Ptr<cv::M…

车牌定位之MSER — 文本检测

最大稳定极值区域&#xff08;MSER-Maximally Stable Extremal Regions&#xff09;可以用于图像的斑点区域检测。它是基于分水岭的概念。 SIFT和SURF算法高效实现了具有尺度和旋转不变性的特征检测&#xff0c;但这些特征不具有仿射不变性。区域检测针对各种不同形状的图像区域…

图像局部特征(十四)--MSER特征

原文: http://blog.csdn.net/zhaocj/article/details/40742191 最大稳定极值区域&#xff08;MSER-Maximally Stable Extremal Regions&#xff09;可以用于图像的斑点区域检测。该算法最早是由Matas等人于2002年提出&#xff0c;它是基于分水岭的概念。 MSER的基本原理是对…

【AI实战】手把手教你深度学习文字识别(文字检测篇:基于MSER, CTPN, SegLink, EAST等方法)

文字检测是文字识别过程中的一个非常重要的环节&#xff0c;文字检测的主要目标是将图片中的文字区域位置检测出来&#xff0c;以便于进行后面的文字识别&#xff0c;只有找到了文本所在区域&#xff0c;才能对其内容进行识别。 文字检测的场景主要分为两种&#xff0c;一种是…

MSRCR

带色彩恢复的多尺度视网膜增强算法&#xff08;MSRCR&#xff09;的原理、实现及应用。 Retinex这个词是由视网膜(Retina)和大脑皮层(Cortex) 两个词组合构成的。Retinex理论主要包含了两个方面的内容&#xff1a;物体的颜色是由物体对长波、 中波和短波光线的反射能力决定的&a…

mser场景文字检测及筛选

发现很多人都用mser,swt等进行场景文字的检测&#xff0c;最近也去实现了一下&#xff0c;虽然swt较新的算法&#xff0c;但实现过程中传统的mser算法反而更稳定&#xff0c;速度也会更快&#xff0c;可能是我还没有完全领会swt的精髓。ps:暂时只做水平及水平倾斜的文字 demo …

mser python篇

之前一直在matlab上用这个函数&#xff0c;现在转移到python上面使用 ———————————————————————————————————— 1、代码 I cv2.cvtColor(I, cv2.COLOR_BGR2GRAY); mser cv2.MSER_create() regions,boxes mser.detectRegions(I) for bo…

MSER — 自然场景文本检测

MSER是最大稳定极值区域&#xff1a;是对一幅灰度图像&#xff08;灰度值为0&#xff5e;255&#xff09;取阈值进行二值化处理&#xff0c;阈值从0到255依次递增。阈值的递增类似于分水岭算法中的水面的上升&#xff0c;随着水面的上升&#xff0c;有一些较矮的丘陵会被淹没&a…

文字检测与识别1-MSER

导语 文字识别在现实场景中的用途非常广泛&#xff0c;现在已经有很多公司将这项技术用于实际中。比如车牌识别&#xff0c;图片转换成文档&#xff0c;拍照搜题&#xff0c;拍照翻译等。这让很多人有了错觉&#xff0c;感觉文字识别的技术已经炉火纯青&#xff0c;可以广泛应…

MSER常见参数

MSER用于文本检测已经成熟了&#xff0c;现简单使用来识别车牌号。 目录 MSER参数最大最小区域固定 MSER参数 默认&#xff1a;int delta 5, int min_area 60, int max_area 14400, double max_variation 0.25, double min_diversity .2 * Full constructor for %MSER d…

MSER算法

最稳定极值区域介绍 如把灰度图看成高低起伏的地形图&#xff0c;其中灰度值看成海平面高度的话&#xff0c;MSER的作用就是在灰度图中找到符合条件的坑洼。条件为坑的最小高度&#xff0c;坑的大小&#xff0c;坑的倾斜程度&#xff0c;坑中如果已有小坑时大坑与小坑的变化率…

【MSER】基于MSER算法的交通标志分割仿真

1.软件版本 MATLAB2021a 2.本算法理论知识 [1]钱坤. 基于MSER和遗传优化SVM的交通标志识别的研究[D]. 大连理工大学. [2]王斌, 常发亮, 刘春生. 基于MSER和SVM的快速交通标志检测[J]. 光电子.激光, 2016. 3.部分源码 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%…

MSER相关总结

最近做项目用到了MSER&#xff0c;特地在这做总结。 以前提到字符检测首先会想到Tesseract&#xff0c;但是tesseact对图像的二值化要求过高&#xff0c;比较适合于白底黑字的字符识别&#xff0c;对于复杂情况就无能为力了&#xff1b; 于是就想到用轮廓检测&#xff0c;这种…

最大稳定极值区域(MSER)检测

Lowe和Bay提出的SIFT和SURF算法高效实现了具有尺度和旋转不变性的特征检测&#xff0c;但这些特征不具有仿射不变性。 区域检测针对各种不同形状的图像区域&#xff0c;通过对区域的旋转和尺寸归一化&#xff0c;可以实现仿射不变性。 MSER&#xff08;Maximally Stable Extr…

MSER最稳定极值区域源码分析

最稳定极值区域介绍 如把灰度图看成高低起伏的地形图&#xff0c;其中灰度值看成海平面高度的话&#xff0c;MSER的作用就是在灰度图中找到符合条件的坑洼。条件为坑的最小高度&#xff0c;坑的大小&#xff0c;坑的倾斜程度&#xff0c;坑中如果已有小坑时大坑与小坑的变化率。…

OpenCVSharp入门教程 特征提取①——MSER区域特征提取Maximally Stable Extremal Regions

文章目录 一、前文二、特征提取流程三、界面布局四、功能实现4.1 打开图片4.2 特征提取—源码4.3 特征提取—参数讲解4.4 特征提取—Detect和DetectRegions 五、运行效果图六、发现并解决问题七、其他问题 一、前文 MSER Maximally Stable Extremal Regions 最大极值稳定区 业…