最大稳定极值区域(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
如把灰度图看成高低起伏的地形图,其中灰度值看成海平面高度的话,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代表着是图片中一个连通(比如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如下:
那怎么判断一个ER是不是MSER呢?对于单通道图像来说主要有五个参数,delta, maxVariation,minDiversity, minArea, maxArea.其中minArea,maxArea在opencv中代表的点数,如ID11的点数是3,ID10的点数是1。
static Ptr 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
#include
#include
#include
#include
using namespace cv;
using namespace std;
std::vector mserGetPlate(cv::Mat srcImage)
{
// HSV空间转换
cv::Mat gray, gray_neg;
cv::Mat hsi;
cv::cvtColor(srcImage, hsi, CV_BGR2HSV);
// 通道分离
std::vector channels;
cv::split(hsi, channels);
// 提取h通道
gray = channels[1];
// 灰度转换
cv::cvtColor(srcImage, gray, CV_BGR2GRAY);
// 取反值灰度
gray_neg = 255 - gray;
std::vector > regContours;
std::vector > charContours;
// 创建MSER对象
cv::Ptr mesr1 = cv::MSER::create(2, 10, 5000, 0.5, 0.3);
cv::Ptr mesr2 = cv::MSER::create(2, 2, 400, 0.1, 0.3);
std::vector bboxes1;
std::vector 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& r = regContours[i];
for (int j = 0; j < (int)r.size(); j++)
{
cv::Point pt = r[j];
mserMapMat.at(pt) = 255;
}
}
// MSER- 检测
for (int i = (int)charContours.size() - 1; i >= 0; i--)
{
// 根据检测区域点生成mser-结果
const std::vector& r = charContours[i];
for (int j = 0; j < (int)r.size(); j++)
{
cv::Point pt = r[j];
mserNegMapMat.at(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 > plate_contours;
cv::findContours(mserClosedMat, plate_contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0));
// 候选车牌区域判断输出
std::vector 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 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
#include
#include
#include
#include
#include
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(j) = countNonZero(data); //统计这一行或一列中,非零元素的个数,并保存到mhist中
}
//Normalize histogram
double 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 features
Mat vhist = ProjectedHistogram(in, VERTICAL);
Mat hhist = ProjectedHistogram(in, HORIZONTAL);
//Last 10 is the number of moments components
int 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(j) = vhist.at(i);
j++;
}
for (int i = 0; i(j) = hhist.at(i);
j++;
}
return out;
}
// ! EasyPR的getFeatures回调函数!本函数是获取垂直和水平的直方图图值
void getHistogramFeatures(const Mat& image, Mat& features)
{
features = getTheFeatures(image);
}
std::vector 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 > regContours;
std::vector > 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 mesr1 = cv::MSER::create(delta, minArea, int(maxAreaRatio * imageArea), 0.15, 10);
cv::Ptr mesr2 = cv::MSER::create(delta, minArea, 400, 0.1, 0.3);
std::vector bboxes1;
std::vector 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& r = regContours[i];
for (int j = 0; j < (int)r.size(); j++)
{
cv::Point pt = r[j];
mserMapMat.at(pt) = 255;
}
}
//MSER- 检测
for (int i = (int)charContours.size() - 1; i >= 0; i--)
{
// 根据检测区域点生成mser-结果
const std::vector& r = charContours[i];
for (int j = 0; j < (int)r.size(); j++)
{
cv::Point pt = r[j];
mserNegMapMat.at(pt) = 255;
}
}
imshow("mserMapMat", mserMapMat);
//imshow("mserNegMapMat", mserNegMapMat);
cv::Mat mserResMat;
mserResMat = mserMapMat;
mserResMat = mserMapMat & mserNegMapMat; // mser+与mser-位与操作
//imshow("mserResMat", mserResMat);
// 寻找外部轮廓
std::vector > plate_contours;
cv::findContours(mserMapMat, plate_contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0));
// 候选车牌区域判断输出
std::vector 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 candidates;
candidates = mserGetPlate(srcImage);
Ptr ann = cv::ml::ANN_MLP::load("ann.xml");// 120 40 65
Mat 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 |
bboxes | 产生边界框,vector |
from:https://blog.csdn.net/hust_bochu_xuchao/article/details/52230694