最大稳定极值区域(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.csdn.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.csdn.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栈,并弹出最小边界(0,2)作为当前点;

边界水位为3,并观察ER栈的gray level,ID2和ID3都小于边界水位,因此合并ER栈的两个ER

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

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

构建树MSER Tree如下:

OpenCV实践:

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

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,// 当前区域与稳定区域的变化率// MSCR使用
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水位差间两个区域面积是否满足一定条件 maxVariation;

还有一个判断条件如图C,如果已经有一个候选区域R_stable了,R_candidate是否可以作为一个极值区域,也就是大坑套小坑的情况,minDiversity。

maxVariation是上图B的情况,值为公式A;

minDiversity是上图C的情况是为了解决两个MSER靠的很近的问题,值为公式B:

MSER的核心思想都是要找到一块区域,能跟周围的有明显的变化。在MSER里,这个是通过maxvariation定义的。

,其中S代表的ER的点数,就是公式A

以下图为例,比如delta= 2,要计算ID1的maxvatiation,可以看出S(ERlevel) =9,ID1的gray level是3,因此要找到3-2 =1 gray level的ER,我们去history中找gray level=1的点数最多ID,找到ID10,ID12,都是1,因此按照上面的公式,求(9-1)/1,maxvariation是8.而opencv默认maxvariation是0.25。

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

最后如果我们对上面的ER tree做假设,只有ID10,11,13是MSER,那么我们的MSER tree就分成了两个Tree.

                   

有点看懂了,呜呜呜呜呜呜


自然场景下文字检测一般分为以下这么几步,产生候选(candidate),字符过滤,字符合并成文本行,文本行过滤和后处理。需要注意的是有些论文采用字符和文本行双重过滤,有些论文则只采用其中一种过滤。

产生候选(Extract candidates)

很多文章都采用连通域类方案,如SWT采用的连通域,RST和EST采用的MSER,但是大部分用的还是MSER类的,虽然在ICDAR的数据库中还有一些字母MSER检测不出来,但是从性能和效果上说,MSER还是具有一些优势(请注意以下所讲的都是灰度图的MSER,彩色图的MSER用的是不同的算法)。

实现过程大概分为下面几步:

<1>mser文字获选区域块的提取,包括反白字体

<2>对mser文字获选区域进行连通域分析,求取最小包含矩形框,对矩形框进行合并。主要还是传统的大小距离,角度等的关系

<3>对合并的矩形框进行再次合并,得到一个个文字块

<4>将文字块处理成正的矩形块,进行块的反白判断及二值化

<5>对二值化后的图像进行投影及文字宽高大小分析,判断是否为文字块。得到最终结果

from:文字检测与识别1-MSER

Mser车牌目标检测示例 完整的C++代码,代码旨在了解MSER如何使用,应用效果一般。


// Mser车牌目标检测
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <iostream>
using namespace cv;
using namespace std;std::vector<cv::Rect> mserGetPlate(cv::Mat srcImage)
{// HSV空间转换cv::Mat gray, gray_neg;cv::Mat hsi;cv::cvtColor(srcImage, hsi, CV_BGR2HSV);// 通道分离std::vector<cv::Mat> channels;cv::split(hsi, channels);// 提取h通道gray = channels[1];// 灰度转换 cv::cvtColor(srcImage, gray, CV_BGR2GRAY);// 取反值灰度gray_neg = 255 - gray;std::vector<std::vector<cv::Point> > regContours;std::vector<std::vector<cv::Point> > charContours;// 创建MSER对象cv::Ptr<cv::MSER> mesr1 = cv::MSER::create(2, 10, 5000, 0.5, 0.3);cv::Ptr<cv::MSER> mesr2 = cv::MSER::create(2, 2, 400, 0.1, 0.3);std::vector<cv::Rect> bboxes1;std::vector<cv::Rect> bboxes2;// MSER+ 检测mesr1->detectRegions(gray, regContours, bboxes1);// MSER-操作mesr2->detectRegions(gray_neg, charContours, bboxes2);cv::Mat mserMapMat = cv::Mat::zeros(srcImage.size(), CV_8UC1);cv::Mat mserNegMapMat = cv::Mat::zeros(srcImage.size(), CV_8UC1);for (int i = (int)regContours.size() - 1; i >= 0; i--){// 根据检测区域点生成mser+结果const std::vector<cv::Point>& r = regContours[i];for (int j = 0; j < (int)r.size(); j++){cv::Point pt = r[j];mserMapMat.at<unsigned char>(pt) = 255;}}// MSER- 检测for (int i = (int)charContours.size() - 1; i >= 0; i--){// 根据检测区域点生成mser-结果const std::vector<cv::Point>& r = charContours[i];for (int j = 0; j < (int)r.size(); j++){cv::Point pt = r[j];mserNegMapMat.at<unsigned char>(pt) = 255;}}// mser结果输出cv::Mat mserResMat;// mser+与mser-位与操作mserResMat = mserMapMat & mserNegMapMat;cv::imshow("mserResMat", mserResMat);// 闭操作连接缝隙cv::Mat mserClosedMat;cv::morphologyEx(mserResMat, mserClosedMat,cv::MORPH_CLOSE, cv::Mat::ones(1, 20, CV_8UC1));cv::imshow("mserClosedMat", mserClosedMat);// 寻找外部轮廓std::vector<std::vector<cv::Point> > plate_contours;cv::findContours(mserClosedMat, plate_contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0));// 候选车牌区域判断输出std::vector<cv::Rect> candidates;for (size_t i = 0; i != plate_contours.size(); ++i){// 求解最小外界矩形cv::Rect rect = cv::boundingRect(plate_contours[i]);// 宽高比例double wh_ratio = rect.width / double(rect.height);// 不符合尺寸条件判断if (rect.height > 20 && wh_ratio > 4 && wh_ratio < 7)candidates.push_back(rect);}return  candidates;
}
int main()
{cv::Mat srcImage =cv::imread("plate1.jpg");if (srcImage.empty())return-1;cv::imshow("src Image", srcImage);// 候选车牌区域检测std::vector<cv::Rect> candidates;candidates = mserGetPlate(srcImage);// 车牌区域显示for (int i = 0; i < candidates.size(); ++i){cv::imshow("rect", srcImage(candidates[i]));cv::waitKey();}cv::waitKey(0);return 0;
}

我的改进:


// Mser车牌目标检测
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <iostream>
#include<opencv2\ml\ml.hpp>
using namespace cv;
using namespace std;// !获取垂直和水平方向直方图
Mat ProjectedHistogram(Mat img, int t)
{int sz = (t) ? img.rows : img.cols;Mat mhist = Mat::zeros(1, sz, CV_32F);for (int j = 0; j<sz; j++) {Mat data = (t) ? img.row(j) : img.col(j);mhist.at<float>(j) = countNonZero(data); //统计这一行或一列中,非零元素的个数,并保存到mhist中}//Normalize histogramdouble min, max;minMaxLoc(mhist, &min, &max);if (max>0)mhist.convertTo(mhist, -1, 1.0f / max, 0);//用mhist直方图中的最大值,归一化直方图return mhist;
}
//! 获得车牌的特征数
Mat getTheFeatures(Mat in)
{const int VERTICAL = 0;const int HORIZONTAL = 1;//Histogram featuresMat vhist = ProjectedHistogram(in, VERTICAL);Mat hhist = ProjectedHistogram(in, HORIZONTAL);//Last 10 is the number of moments componentsint numCols = vhist.cols + hhist.cols;Mat out = Mat::zeros(1, numCols, CV_32F);//Asign values to feature,样本特征为水平、垂直直方图int j = 0;for (int i = 0; i<vhist.cols; i++){out.at<float>(j) = vhist.at<float>(i);j++;}for (int i = 0; i<hhist.cols; i++){out.at<float>(j) = hhist.at<float>(i);j++;}return out;
}// ! EasyPR的getFeatures回调函数!本函数是获取垂直和水平的直方图图值
void getHistogramFeatures(const Mat& image, Mat& features)
{features = getTheFeatures(image);
}std::vector<cv::Rect> mserGetPlate(cv::Mat srcImage)
{// HSV空间转换cv::Mat gray, gray_neg;// 灰度转换 cv::cvtColor(srcImage, gray, CV_BGR2GRAY);imshow("gray", gray);// 取反值灰度gray_neg = 255 - gray;std::vector<vector<Point> > regContours;std::vector<vector<Point> > charContours;//点集// 创建MSER对象int imageArea = gray.rows * gray.cols;int delta = 1;//const int delta = CParams::instance()->getParam2i();;const int minArea = 30;double maxAreaRatio = 0.001;cv::Ptr<cv::MSER> mesr1 = cv::MSER::create(delta, minArea, int(maxAreaRatio * imageArea), 0.15, 10);cv::Ptr<cv::MSER> mesr2 = cv::MSER::create(delta, minArea, 400, 0.1, 0.3);std::vector<cv::Rect> bboxes1;std::vector<cv::Rect> bboxes2;// MSER+ 检测mesr1->detectRegions(gray, regContours, bboxes1);// MSER-操作mesr2->detectRegions(gray_neg, charContours, bboxes2);cv::Mat mserMapMat = cv::Mat::zeros(srcImage.size(), CV_8UC1);cv::Mat mserNegMapMat = cv::Mat::zeros(srcImage.size(), CV_8UC1);for (int i = (int)regContours.size() - 1; i >= 0; i--){// 根据检测区域点生成mser+结果const std::vector<cv::Point>& r = regContours[i];for (int j = 0; j < (int)r.size(); j++){cv::Point pt = r[j];mserMapMat.at<unsigned char>(pt) = 255;}}//MSER- 检测for (int i = (int)charContours.size() - 1; i >= 0; i--){// 根据检测区域点生成mser-结果const std::vector<cv::Point>& r = charContours[i];for (int j = 0; j < (int)r.size(); j++){cv::Point pt = r[j];mserNegMapMat.at<unsigned char>(pt) = 255;}}imshow("mserMapMat", mserMapMat);//imshow("mserNegMapMat", mserNegMapMat);cv::Mat mserResMat;mserResMat = mserMapMat;mserResMat = mserMapMat & mserNegMapMat; // mser+与mser-位与操作//imshow("mserResMat", mserResMat);// 寻找外部轮廓std::vector<std::vector<Point> > plate_contours;cv::findContours(mserMapMat, plate_contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0));// 候选车牌区域判断输出std::vector<cv::Rect> candidates;for (size_t i = 0; i != plate_contours.size(); ++i){// 求解最小外界矩形cv::Rect rect = cv::boundingRect(plate_contours[i]);// 宽高比例double wh_ratio = rect.width / double(rect.height);if ( wh_ratio > 0.2 && wh_ratio < 0.9)candidates.push_back(rect);}return  candidates;
}
int main()
{cv::Mat srcImage =cv::imread("plate1.jpg");if (srcImage.empty())return-1;cv::imshow("src Image", srcImage);// 候选车牌区域检测std::vector<cv::Rect> candidates;candidates = mserGetPlate(srcImage);Ptr<ml::ANN_MLP> ann = cv::ml::ANN_MLP::load("ann.xml");// 120 40 65Mat feature,gray,feature2,dst;Mat out(1, 65, CV_32F);cvtColor(srcImage, gray, CV_BGR2GRAY);for (int i = 0; i < candidates.size(); ++i){Mat result = gray(candidates[i]);resize(result, dst, Size(10, 10));getHistogramFeatures(dst, feature);Mat dst2=dst.reshape(0, 1);//cn: 表示通道数, 如果设为0,则表示保持通道数不变,否则则变为设置的通道数。//rows: 表示矩阵行数。 如果设为0,则表示保持原有的行数不变,否则则变为设置的行数。dst2.convertTo(dst2, CV_32F);cv::hconcat(dst2, feature, feature2);float reponse=ann->predict(feature2,out);//reponse返回最大值double minVal; double maxVal; Point minLoc; Point maxLoc;minMaxLoc(out, &minVal, &maxVal, &minLoc, &maxLoc);imshow("dst", srcImage(candidates[i]));}// 显示检测到所有字符区域for (int i = 0; i < candidates.size(); ++i){string image = "rect" + cv::format("%.4d", i);namedWindow(image,0);cv::imshow(image, srcImage(candidates[i]));}cv::waitKey(0);return 0;
}

字符区域如下:

将所有字符区域送入ANN模型判别:

得分大于0.9,则判定是字符:

float reponse=ann->predict(feature2,out);//reponse返回最大分数
        double minVal; double maxVal; Point minLoc; Point maxLoc;
        minMaxLoc(out, &minVal, &maxVal, &minLoc, &maxLoc);
        if (maxVal > 0.9)
            candidates2.push_back(candidates[i]);

输出:

然后根据输出的矩形框的距离,位置,判别车牌位置。


virtual void cv::MSER::detectRegions (InputArray image,std::vector< std::vector< Point > > & msers,std::vector< Rect > & bboxes )  

Parameters

image input image (8UC1, 8UC3 or 8UC4, 尺寸大于等于 3x3),可以是彩色,使用MSEC算法
msers 得到的点集列表,vector<vector<Point> >
bboxes 产生边界框,vector<cv::Rect>

from:https://blog.csdn.net/hust_bochu_xuchao/article/details/52230694

车牌定位之MSER — 文本检测相关推荐

  1. mser python车牌识别_基于MSER与SVM算法的车牌定位识别方法

    基于 MSER 与 SVM 算法的车牌定位识别方法 胡成伟 ; 袁明辉 [期刊名称] <软件> [年 ( 卷 ), 期] 2020(041)002 [摘要] 针对实际车牌识别系统中车牌位置 ...

  2. ABCNet 精读:使用自适应贝塞尔曲线网络进行进行实时场景文本定位 OCR 文本定位 文本检测 CVPR

    文章目录 ABCNet 导读:使用自适应贝塞尔曲线网络进行进行实时场景文本定位 ABCNet:Real-time Scene Text Spotting with Adaptive Bezier-Cu ...

  3. MSER+NMS 文本检测(身份证+发票+火车票)

    版权     此篇文章不细说MSER和NMS原理,以实战为主.        MSER是最大稳定极值区域:是对一幅灰度图像(灰度值为0-255)取阈值进行二值化处理,阈值从0到255依次递增.阈值的递 ...

  4. MSER — 自然场景文本检测

    MSER是最大稳定极值区域:是对一幅灰度图像(灰度值为0-255)取阈值进行二值化处理,阈值从0到255依次递增.阈值的递增类似于分水岭算法中的水面的上升,随着水面的上升,有一些较矮的丘陵会被淹没,如 ...

  5. 基于EAST和Tesseract的文本检测与识别

    目录 导言 现实世界问题 说明 问题陈述 业务目标和约束条件 可用于文本检测和识别的数据集 数据集概述和说明 探索性数据分析(EDA) 深度学习时代之前的文本检测方法 EAST(高效精确的场景文本检测 ...

  6. 值得一看的文本检测方法

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 前  言 目前深度学习方法做文本检测比较普遍,但是也存在一些时候G ...

  7. 实战:车牌识别之车牌定位

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 本文转自|3D视觉工坊 从本节起,我们开始尝试做一下车牌识别中的算 ...

  8. 人工智能学习--文本检测实践

    注释:文本检测 和 文本识别是两回事. 可能现在已经有 end-to-end的深度神经网络可以将文本检测和识别一起实现,这个要去搜相关的sci论文. 文本检测,是从一张图片中找到文字区域,并用矩形框标 ...

  9. 自然场景文本检测识别技术集合(转)

    本文及其它机器学习.深度学习算法的全面系统讲解可以阅读<机器学习与应用>,清华大学出版社,雷明著,由SIGAI公众号作者倾力打造,自2019年1月出版以来已重印3次. 书的购买链接 书的勘 ...

最新文章

  1. python全栈开发,Day40(进程间通信(队列和管道),进程间的数据共享Manager,进程池Pool)...
  2. kafka partition分配_kafka的分区分配策略
  3. 配置https后访问返回403 forbidden
  4. .NET Core实战项目之CMS 第六章 入门篇-Vue的快速入门及其使用
  5. GridView CSS的样式表
  6. [转]Python爬虫html解析工具beautifulSoup在pycharm中安装及失败的解决办法
  7. 阿尔伯塔大学的计算机科学专业好吗,去阿尔伯塔大学留学这些专业千万不能错过!...
  8. session过期问题
  9. 渗透测试web安全 - webshell 免杀 绕过waf总结
  10. java单例模式使用_Java单例模式的应用
  11. 华为手机怎么修改dns服务器,华为域名解析修改dns
  12. DevOps知识地图
  13. SpringCloud之服务网关(总体第五篇)
  14. microbit python积木编程_TurnipBit—MicroPython开发板:从积木式编程语言开始学做小小创客...
  15. 江湖2 java_热血江湖2手游一键端源码搭建教程+APP端+WEB端+GM工具
  16. 智慧城市指挥调度系统软件解决方案
  17. CodeM资格赛C 优惠券 题解
  18. 金蝶Kis旗舰版不能反过账?
  19. 开发工具篇第九讲:菜鸟入坑指南
  20. 我愿称之为史上最全的深度学习面经总结(附答案详解)

热门文章

  1. SecureCRT for mac的坑
  2. 升级OpenSSL修复高危漏洞Heartbleed
  3. 【Promise】基本使用+回调地狱+宏队列与微队列
  4. html5手机壳,这款帅炸的手机壳,能够让你的iPhone秒变安卓
  5. 华为1+x拓扑图资源
  6. esp32-C3学习笔记(1)微信配网
  7. 定个小目标,开始更新博客,再唠嗑一下现在的一些心路
  8. java 8实战 异步社区_服!看完阿里大牛手写的Java异步编程实战笔记,我惊呆了...
  9. csdn中c币、积分 获取方法
  10. 江苏省环保厅数据中心同城灾备建设项目