MSER — 文本检测
最大稳定极值区域(MSER-Maximally Stable Extremal Regions)可以用于图像的斑点区域检测。它是基于分水岭的概念。
SIFT和SURF算法高效实现了具有尺度和旋转不变性的特征检测,但这些特征不具有仿射不变性。区域检测针对各种不同形状的图像区域,通过对区域的旋转和尺寸归一化,可以实现仿射不变性。MSER(Maximally Stable Extrernal Regions)是区域检测中影响最大的算法 。
MSER的基本原理是对一幅灰度图像(灰度值为0~255)取阈值进行二值化处理,阈值从0到255依次递增。阈值的递增类似于分水岭算法中的水面的上升,随着水面的上升,有一些较矮的丘陵会被淹没,如果从天空往下看,则大地分为陆地和水域两个部分,这类似于二值图像。在得到的所有二值图像中,图像中的某些连通区域变化很小,甚至没有变化,则该区域就被称为最大稳定极值区域。这类似于当水面持续上升的时候,有些被水淹没的地方的面积没有变化。它的数学定义为:
其中,Qi表示阈值为i时的某一连通区域,Δ表示微小的阈值变化(注水),v(i)为阈值是i时的区域Qi的变化率。当vi小于给定阈值时认为该区域Qi为MSER。显然,这样检测得到的MSER内部灰度值是小于边界的,想象一副黑色背景白色区域的图片,显然这个区域是检测不到的。因此对原图进行一次MSER检测后需要将其反转,再做一次MSER检测,两次操作又称MSER+和MSER-。
MSER的基本思路很简单,但编码实现是很需要算法和编程技巧的。David Nister等人提出了Linear Time Maximally Stable Extremal Regions算法,该算法要比原著提出的算法快,opencv就是利用该算法实现MSER的,opencv 不是利用公式1计算MSER的,而是利用更易于实现的改进方法:
David Nister提出的算法是基于改进的分水岭算法,即当往一个固定的地方注水的时候,只有当该地方的沟壑被水填满以后,水才会向其四周溢出,随着注水量的不断增加,各个沟壑也会逐渐被水淹没,但各个沟壑的水面不是同时上升的,它是根据水漫过地方的先后顺序,一个沟壑一个沟壑地填满水,只有当相邻两个沟壑被水连通在一起以后,水面对于这两个沟壑来说才是同时上升的。该算法的具体步骤如下:
1、初始化栈和堆,栈用于存储组块(组块就是区域,就相当于水面,水漫过的地方就会出现水面,水面的高度就是图像的灰度值,因此用灰度值来表示组块的值),堆用于存储组块的边界像素,相当于水域的岸边,岸边要高于水面的,因此边界像素的灰度值一定不小于它所包围的区域(即组块)的灰度值。首先向栈内放入一个虚假的组块,当该组块被弹出时意味着程序的结束;
2、把图像中的任意一个像素(一般选取图像的左上角像素)作为源像素,标注该像素为已访问过,并且把该像素的灰度值作为当前值。这一步相当于往源像素这一地点注水;
3、向栈内放入一个空组块,该组块的值是当前值;
4、按照顺序搜索当前值的4-领域内剩余的边缘,对于每一个邻域,检查它是否已经被访问过,如果没有,则标注它为已访问过并检索它的灰度值,如果灰度值不小于当前值,则把它放入用于存放边界像素的堆中。另一方面,如果领域灰度值小于当前值,则把当前值放入堆中,而把领域值作为当前值,并回到步骤3;
5、累计栈顶组块的像素个数,即计算区域面积,这是通过循环累计得到的,这一步相当于水面的饱和;
6、弹出堆中的边界像素。如果堆是空的,则程序结束;如果弹出的边界像素的灰度值等于当前值,则回到步骤4;
7、从堆中得到的像素值会大于当前值,因此我们需要处理栈中所有的组块,直到栈中的组块的灰度值大于当前边界像素灰度值为止。然后回到步骤4。
至于如何处理组块,则需要进入处理栈子模块中,传入该子模块的值为步骤7中从堆中提取得到的边界像素灰度值。子模块的具体步骤为:
1)、处理栈顶的组块,即根据公式2计算最大稳定区域,判断其是否为极值区域;
2)、如果边界像素灰度值小于距栈顶第二个组块的灰度值,那么设栈顶组块的灰度值为边界像素灰度值,并退出该子模块。之所以会出现这种情况,是因为在栈顶组块和第二个组块之间还有组块没有被检测处理,因此我们需要改变栈顶组块的灰度值为边界像素灰度值(相当于这两层的组块进行了合并),并回到主程序,再次搜索组块;
3)、弹出栈顶组块,并与目前栈顶组块合并;
4)、如果边界像素灰度值大于栈顶组块的灰度值,则回到步骤1。
在opencv2.4.9中,MSER算法是用类的方法给出的:https://blog.****.net/zhaocj/article/details/40742191
OpenCV MSER(最大极值稳定区域)
如把灰度图看成高低起伏的地形图,其中灰度值看成海平面高度的话,MSER的作用就是在灰度图中找到符合条件的坑洼。条件为坑的最小高度,坑的大小,坑的倾斜程度,坑中如果已有小坑时大坑与小坑的变化率。
左图展示了几种不同的坑洼,根据最小高度,大小,倾斜程度这些条件的不同,选择的坑也就不同。
右图展示了最后一个条件,大坑套小坑的情况。根据条件的不同,选择也不同。
以上便是对坑的举例,MSER主要流程就三部分组成:
1.预处理数据
2.遍历灰度图
3.判断一个区域(坑洼)是否满足条件
简单来说,就如将水注入这个地形中。水遇到低处就往低处流,如果没有低处了,水位就会一点点增长,直至淹没整个地形。在之前预处理下数据,在水位提高时判断下是否满足条件。
预处理数据
先说下流程中的主要部件,如下:
1.img图像,由原8位单通道灰度图转化的更容易遍历和记录数据的32位单通道图。预处理内容为:
32位值记录从这点是否探索过,探索过的方向,灰度值;图大小也扩大了,最外添加了一个像素的完整一圈,值为-1可看作墙,宽度也改变为2的整数次方,用于加快运算。
2.heap边界,记录坑洼边界的堆栈,每个灰度值都有自己的堆栈。预处理内容为:
计算所有灰度值的个数,这样提前就可以分配堆栈大小。例如知道了灰度2的像素由4个,就可以将灰度2的堆栈大小分配为5(多一个位标志位空)。
3.comp(ER栈),记录水坑数据的堆栈,有水位值(灰度值),面积(像素个数和像素位置)等。预处理内容为:
仅仅是分配内存,分配257个(0-255外多一个用作结束)
4.history,记录水位抬高的历史,就是一个小坑抬高水位后一点点变成大坑的历史。预处理内容为:
仅仅是分配内存,大小为像素点个数(就是宽*高)。可以想成所有点都不同都可以形成历史的最大个数。
遍历灰度图ER
ER代表着是图片中一个连通(比如4连通或8连通)区域的集合,此集合内所有的像素值都小于等于某一值,而这个区域内的边界都大于这个值。我们可以把像素的值想象成地势,而把一个ER想象成一个填满水的坑洼的水坑(在这里我们采用4连通)。在这个水坑里,有一个水位淹没了所里面所有的像素但,也就是说这个区域里所有的地势(像素值)都要低于这个水位,并且水也流不出去,因为水盆有个边缘(边缘像素值要高于这个水位)。虽然水流的方式跟现实中有些区别,但是大体意思是一致的。
下面举例子,走下遍历的流程(并不是依次就是一步,一些步骤合并了)(红色为有变动位置):
1、中上图为要遍历的灰度图。左下history是抬高水位的历史,存的是一个ER从低水位到高水位的过程,所有的ER(除了全图)都会存于这个history中。中下comp是水位数据,即当前ER区域。预先入栈一个256的灰度作为顶,用来抬高水位时判断边界值小还是上一个水位数据的灰度值小。右下heap是边界,边界存储的是与当前ER连接的边界坐标,也就是水盆边界的位置。heap_start是每个灰度指向heap堆栈的指针。特殊说明下,heap是一个个堆栈连接在一起的一个数组,由于上面说的预处理过了,已经知道每个灰度的像素个数,所以提前指定了heap_start中每个灰度指向heap中的位置,指向0代表所在堆栈没有数据。例如灰度2有4个像素,所以灰度3的指针从灰度2指针后5个后开始,4个是像素数,1个是代表空的0。
2、黄色位置代表当前像素,如果某个位置被灰色填充,代表这个像素已经被访问。这部分主要是些初始化的工作。主要的意思是我们在该像素点上放充分量的水,水位的值也就等于当前的像素值。从A1位置开始,comp(ER)中入栈一个灰度2的数据,并将heap_cur当前指针设置为2灰度的指针。现在有水停留在黄色位置A1,并且水位为2。人往高处走,水往低处流。在这里唯一的不同是水每次只流向一个方向,而不能同时扩散 ,探索A1右边的B1,标识为已发现。水尝试往流到B1,发现那里的地势为2,B1的值2没有小于当前水位值2,作为边界入栈。把B1加入到地势为2的heap边缘中。
3、同理现在水尝试往A2流,地势为1的像素。值1小于当前水位2,很显然我们的水位可以流向那,这时我们的水位降低为1,先增一个(comp)ER区,入栈水位数据1。而地势为2的A1成了边界,将A1入栈边界栈,调整边界指针heap_cur为指向地势=1的指针,当前像素为A2。
4、探索A2右边B2与下边A3,都没有比当前水位1小,水尝试流向B2和A3,但是流不通,所以将B2和A3分别入栈所属灰度的边界栈。
5、A2所有方向都探索完。处的周围全都尝试流通过了,我们确认当前的像素是属于当前的ER,因此将此像素A2压入comp ER栈顶的点集link points。
6、找到地势最低的边界点,在边界栈中找到最小灰度的一个值出栈(图5里边界里有灰度2的和灰度3的,从当前灰度1开始一点点加大所以找到了灰度2),出栈了A3,作为当前点。A3的灰度2,所以抬高水位。记录历史histroy,修改当前水位数据ER区域灰度为2,边界指针heap_cur指向2灰度的堆栈。
让我们回顾一下刚才的情况,刚才的水位A2是1,然后发现边界的最低的地势为2,说明我们已经找到了一个compER,在这个区域已经没有邻域的地势小于等于1,并且边界都大于1.因此我们现在能做的就是提高水位。而且根据ER的定义,高地势的区域会包含连通的低地势区域,因此我们要将其合并。
7、探索A3周边,发现B3,B3的灰度3比当前大作为边界入栈。
8、A3所有方向也都探索完,将A3加入当前水位数据compER区域中(下图有误,heap图中B1下边还有一个A1,图中未显示)。
9、边界中找到A1。由于A1灰度还是2,没有提升水位。将A1作为当前像素。刚刚的A1周围也早就探索完了,将A1从边界出栈,并加入当前水位数据comp中(下图有误,heap图中红色箭头指向的位置0的下面还有一个B1,图中未显示)。
10、在边界中找到了B1,并出栈作为当前像素。B1右边探索到了C1,C1大于B1,作为边界,加入灰度为3的边界栈中。这时,B1周围已经探索完毕,将B1加入当前水位数据compER中。
11、在边界栈中从灰度2开始查找,找到灰度3中C1,出栈并作为当前像素。然后记录历史history,提高当前水位数据comp的灰度值(从2变成3),设置heap_cur指针到灰度3的边界栈
12、从当前像素C1向下找到C2,C2灰度比当前低。将当前像素C1入栈边界栈,新建灰度2的水位数据comp,边界指针heap_cur指向灰度2。
13、探索C2下面最后一个像素C3,C3大于C2,作为边界将C3加入边界栈。将C2加入水位数据comp中
14、需要抬高水位了,从灰度3的边界栈中出栈C3,发现灰度和上一个水位数据comp的灰度一样,需要合并这两个comp数据。添加历史history,合并两个comp数,设置C3为当前像素。
15、最后的C3,C1,B3,B2周围都没有可以探索的像素了,依次出栈加入水位数据
草,没看懂,还有一种解释参见:https://blog.****.net/PeaceInMind/article/details/49933055
从(1,0)开始,流到(1,1),是边界,不能流入,所以将(1,1)压入边界栈;流到(2,0),能流入,此时当前点变成(2,0), 而(1,1)变成了边界,压入边界栈。
从(2,0)流入(2,1),不能流入,其他位置如(1,0)也无法流入,即相邻像素都比(2,0)大,那咋办,流不出去,难道就死在这里了,遇到这种情况,我们将此像素(2,0)压入ER栈顶的点集中。并且我们找到地势最低的边界点(2,1)出栈,作为当前点。
同时把(2,0)压入history,回顾刚才:(2,0)的水位是1,其边界的最低的地势为2,说明我们已经找到了一个ER区域,已经没有邻域的地势小于等于1,并且边界都大于1.因此我们现在能做的就是提高水位(找到地势最低的边界点(2,1)出栈,作为当前点)。而且根据ER的定义,高地势的区域会包含连通的低地势区域,因此我们要将其合并。
从当前点(2,1),流入(2,2),流不到,所以将(2,2)压入边界。此时发现当前点(2,1)的邻域已经都访问过了,将该点(2,1)压入栈顶的ER,同样的,从边界栈,找到地势最低的边界点(1,0)出栈,发现边界的地势跟当前的水位是一样的,因此直接将其作为该当前点。
访问邻域(0,0),压入边界;
此时所有的邻域都已访问,将当前的点(1,0)压入ER栈顶,找到地势最低的边界点(0,0)出栈
水又流不动了,又到了要提高水位的时候,发现ER栈的第二个水位是256,如果提高到256,水位太大了。因此我们将当前的ER保存到history中,并把它的水位提高到当前位置的地势值3。而且到了这一步我们可以检查地势为1的ER是否为MSER了,依旧是Grow History ID 10保存的内容。
从(0,0)访问到地势为2的(0,1),因此水位再次下降,当前点变成(0,1);
从(0,1)流向(0,2),不能流入,因此将(0,2)压入边界栈
此时没有未访问的邻域点,因此将(0,1)压入ER栈,并弹出最小边界(0,2),发现当前的像素还是2,还在一个水位上,因此不需要合并或者升水位
从(0,2)流入(1,2),水位下降,当前点变为(1,2), 而(0,2)变成了边界点,将其压入边界
现在当前点(1,2)的所有邻域点都已访问了,因此将坐标(1,2)压入ER栈,并弹出最小边界(未看完)
上一步中的边界水位比我们的要高,并观察ER栈的gray level,因此现合并栈顶的两个ER
与上面的情况类似,压入当前点到ER栈,弹出边界,并合并栈顶量ER
按照之前的过程,连续压入对角线上的3,已经没有边界了,推出。自此我们找出了所有的ER.
static Ptr<MSER> cv::MSER::create (
int _delta = 5,
int _min_area = 60,
int _max_area = 14400,
double _max_variation = 0.25,
double _min_diversity = .2,
int _max_evolution = 200,
double _area_threshold = 1.01,
double _min_margin = 0.003,
int _edge_blur_size = 5
)
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:
自然场景下文字检测一般分为以下这么几步,产生候选(candidate),字符过滤,字符合并成文本行,文本行过滤和后处理。需要注意的是有些论文采用字符和文本行双重过滤,有些论文则只采用其中一种过滤。
产生候选(Extract candidates)
很多文章都采用连通域类方案,如SWT采用的连通域,RST和EST采用的MSER,但是大部分用的还是MSER类的,虽然在ICDAR的数据库中还有一些字母MSER检测不出来,但是从性能和效果上说,MSER还是具有一些优势(请注意以下所讲的都是灰度图的MSER,彩色图的MSER用的是不同的算法)。
实现过程大概分为下面几步:
<1>mser文字获选区域块的提取,包括反白字体
<2>对mser文字获选区域进行连通域分析,求取最小包含矩形框,对矩形框进行合并。主要还是传统的大小距离,角度等的关系
<3>对合并的矩形框进行再次合并,得到一个个文字块
<4>将文字块处理成正的矩形块,进行块的反白判断及二值化
<5>对二值化后的图像进行投影及文字宽高大小分析,判断是否为文字块。得到最终结果
from:https://blog.****.net/u013293750/article/details/53031893
from:文字检测与识别1-MSER