今天我们来介绍车牌定位中的一种新方法--文字定位方法(MSER),包括其主要设计思想与实现。接着我们会介绍一下EasyPR v1.5-beta版本中带来的几项改动。
一、文字定位法
在EasyPR前面几个版本中,最为人所诟病的就是定位效果不佳,尤其是在面对生活场景(例如手机拍摄)时。由于EasyPR最早的数据来源于卡口,因此对卡口数据进行了优化,而并没有对生活场景中图片有较好处理的策略。后来一个版本(v1.3)增加了颜色定位方法,改善了这种现象,但是对分辨率较大的图片处理仍然不好。再加上颜色定位在面对低光照,低对比度的图像时处理效果大幅度下降,颜色本身也是一个不稳定的特征。因此EasyPR的车牌定位的整体鲁棒性仍然不足。
针对这种现象,EasyPR v1.5增加了一种新的定位方法,文字定位方法,大幅度改善了这些问题。下面几幅图可以说明文字定位法的效果。
图1 夜间的车牌图像(左) , 对比度非常低的图像(右)
图2 近距离的图像(左),高分辨率(3200宽)的图像(右)
文字定位方法是采用了低级过滤器提取文字,然后再将其组合的一种定位方法。原先是利用在场景中定位文字,在这里利用其定位车牌。与在扫描文档中的文字不同,自然场景中的文字具有低对比度,背景各异,光亮干扰较多等情况,因此需要一个极为鲁棒的方法去提取出来。目前业界用的较多的是MSER(最大稳定极值区域)方法。EasyPR使用的是MSER的一个改良方法,专门针对文字进行了优化。
在文字定位出来以后,一般需要用一个分类器将其中大部分的定位错误的文字去掉,例如ANN模型。为了获得最终的车牌,这些文字需要组合起来。由于实际情况的复杂,简单的使用普通的聚类效果往往不好,因此EasyPR使用了一种鲁棒性较强的种子生长方法(seed growing)去组合。
我在这里简单介绍一下具体的实现。关于方法的细节可以看代码,代码较长,放到文末。
步骤流程:
1、首先通过MSER提取区域,提取出的区域进行一个尺寸判断,滤除明显不符合车牌文字尺寸的。
2、接下来使用一个文字分类器(这里是把得到的区域送入ann神经网络,输出每个区域属于文字的概率),将概率大于0.9的设为强种子(下图的绿色方框)。靠近的强种子进行聚合,划出一条线穿过它们的中心(图中白色的线)。一般来说,这条线就是车牌的中间轴线,斜率什么都相同。
3、之后在这条线的附近寻找那些概率低于0.9的弱种子(蓝色方框)。由于车牌的特征,这些蓝色方框应该跟绿色方框距离不太远,同时尺寸也不会相差太大。蓝色方框是在绿色方框的左右查找的,有时候,几个绿色方框中间可能存在着一个方框,这可以通过每个方框之间的距离差推出来,这就是橙色的方框。全部找完以后。绿色方框加上蓝色与橙色方框的总数代表着目前在车牌区域中发现的文字数。
4、有时这个数会低于7(中文车牌的文字数),这是因为有些区域即便通过MSER也提取不到(例如非常不稳定或光照变化大的),另外很多中文也无法通过MSER提取到(中文大多是不连通的,MSER提取的区域基本都是连通的)。所以下面需要再增加一个滑动窗口(红色方框)来寻找这些缺失的文字或者中文,如果分类器概率大于某个阈值,就可以将其加入到最终的结果中。最后把所有文字的位置用一个方框框起来,就是车牌的区域。
想要通过中间图片进行调试程序的话,首先依次根据函数调用关系plateMserLocate->mserSearch->mserCharMatch,在core_func.cpp找到位置。在函数的最后,把图片输出的判断符改为1。然后在resources/image下面依次新建tmp与plateDetect目录(跟代码中的一致),接下来再运行时在新目录里就可以看到这些调试图片。(EasyPR里还有很多其他类似的输出代码,只要按照代码的写法创建文件夹就可以看到输出结果了)。
图3 文字定位的中间结果(调试图像)
二、更加合理准确的评价指标
原先的EasyPR的评价标准中有很多不合理的地方。例如一张图片中找到了一个疑似的区域,就认为是定位成功了。或者如果一张图片中定位到了几个车牌,就用差距率最小的那个作为定位结果。这些地方不合理的地方在于:
- 有可能找到的疑似区域根本不是车牌区域。
- 另外一个包含几个车牌的图片仅仅用最大的一个作为结果,明显不合理。
因此新评价指标需要考虑定位区域和车牌区域的位置差异,只有当两者接近时才能认为是定位成功。另外,一张图片如果有几个车牌,对应的就有几个定位区域,每个区域与车牌做比对,综合起来才能作为定位效果。因此需要加入一个GroundTruth,标记各个车牌的位置信息。新版本中,我们标记了251张图片,其中共250个车牌的位置信息。为了衡量定位区域与车牌区域的位置差的比例,又引入了ICDAR2003的评价协议,来最终计算出定位的recall,precise与fscore值。
字符识别模块则做了小改动。首先是去除了“平均字符差距”这个意义较小的指标。转而用零字符差距,一字符差距,中文字符正确替代,这三者都是比率。零字符差距(0-error)指的是识别结果与车牌没有任何差异,跟原先的评价协议中的“完全正确率”指代一样。一字符差距(1-error)指的是错别仅仅只有1个字符或以下的,包括零字符差距。注意,中文一般是两个字符。中文字符正确(Chinese-precise)指代中文字符识别正确的比率。这三个指标,都是越大越好,100%最高。
为了实际看出这些指标的效果,拿通用测试集里增加的50张复杂图片做对此测试,文字定位方法在这些数据上的表现的差异与原先的SOBEL,COLOR定位方法的区别可以看下面的结果。
SOBEL+COLOR:
总图片数:50, Plates count:52, 定位率:51.9231%
Recall:46.1696%, Precise:26.3273%, Fscore:33.533%.
0-error:12.5%, 1-error:12.5%, Chinese-precise:37.5%
CMSER:
总图片数:50, Plates count:52, 定位率:78.8462%
Recall:70.6192%, Precise:70.1825%, Fscore:70.4002%.
0-error:59.4595%, 1-error:70.2703%, Chinese-precise:70.2703%
可以看出定位率提升了接近27个百分点,定位Fscore与中文识别正确率则提升了接近1倍。
三、非极大值抑制
新版本中另一个较大的改动就是大量的使用了非极大值抑制(Non-maximum suppression)。使用非极大值抑制有几个好处:
1、当有几个定位区域重叠时,可以根据它们的置信度(也是SVM车牌判断模型得出的值)来取出其中最大概率准确的一个,移除其他几个。这样,不同定位方法,例如Sobel与Color定位的同一个区域,只有一个可以保留。因此,EasyPR新版本中,最终定位出的一个车牌区域,不再会有几个框了。
2、结合滑动窗口,可以用其来准确定位文字的位置,例如在车牌定位模块中找到概率最大的文字位置,或者在文字识别模块中,更准确的找到中文文字的位置。
非极大值抑制的使用使得EasyPR的定位方法与后面的识别模块解耦了。以前,每增加定位方法,可能会对最终输出产生影响。现在,无论多少定位方法定位出的车牌都会通过非极大值抑制取出最大概率的一个,对后面的方法没有一点影响。
另外,如今setMaxPlates()这个函数可以确实的作用了。以前可以设置,但没效果(因为以前有好多分别由颜色定位和sobel定位的重叠区域)。现在,设置这个值为n以后,当在一副图像中检测到大于n个车牌区域(注意,这个是经过非极大值抑制后的)时,EasyPR只会输出n个可能性最高的车牌区域。
四、字符分割与识别部分的强化
新版本中字符分割与识别部分都添加了新算法。例如使用了spatial-ostu替代普通的ostu算法,改进了图像分割在面对光照不均匀的图像上的二值化效果。
图4 车牌图像(左),普通大津阈值结果(中),空间大津阈值结果(右)
同时,识别部分针对中文增加了一种adaptive threshold方法。这种方法在二值化“川”字时有比ostu更好的效果。通过将两者一并使用,并选择其中字符识别概率最大的一个,显著提升了中文字符的识别准确率。在识别中文时,增加了一个小型的滑动窗口,以此来弥补通过省份字符直接查找中文字符时的定位不精等现象。
五、新的特征与SVM模型,新的中文识别ANN模型
强化车牌判断的鲁棒性,更改SVM模型的特征,使用LBP(https://blog.csdn.net/qq_30815237/article/details/88541546)特征的模型在面对低对比度与光照的车牌图像中也有很好的判断效果。为了强化中文识别的准确率,现在单独为31类中文文字训练了一个ANN模型ann_chinese,使用这个模型,相对原先的通用模型可以提升近10个百分点。
六、其他
几天前EasyPR发布了1.5-alpha版本。今天发布的beta版本相对于alpha版本,增加了Grid Search功能, 对文字定位方法的参数又进行了部分调优,同时去除了一些中文注释以提高window下的兼容性,除此之外,在速度方面,此版本首次使用了多线程编程技术(OpenMP)来提高算法整体的效率等,使得最终的速度有了2倍左右的提升。
下面说一点新版本的不足:目前来看,文字定位方法的鲁棒性确实很高,不过遗憾的速度跟颜色定位方法相比,还是慢了接近一倍(与Sobel定位效率相当)。后面的改善中,考虑对其进行优化。另外,字符分割的效果实际上还是可以有更多的优化算法选择的,未来的版本可以考虑对其做一个较大的尝试与改进。
文字定位方法代码:
//! 使用验证大小来首先生成char候选者 char candidates
void mserCharMatch(const Mat &src, std::vector<Mat> &match, std::vector<CPlate>& out_plateVec_blue, std::vector<CPlate>& out_plateVec_yellow,
bool usePlateMser, std::vector<RotatedRect>& out_plateRRect_blue, std::vector<RotatedRect>& out_plateRRect_yellow, int img_index,
bool showDebug) {
Mat image = src;
std::vector<std::vector<std::vector<Point>>> all_contours;
std::vector<std::vector<Rect>> all_boxes;
all_contours.resize(2);
all_contours.at(0).reserve(1024);
all_contours.at(1).reserve(1024);
all_boxes.resize(2);
all_boxes.at(0).reserve(1024);
all_boxes.at(1).reserve(1024);
match.resize(2);
std::vector<Color> flags;
flags.push_back(BLUE);
flags.push_back(YELLOW);
const int imageArea = image.rows * image.cols;
const int delta = 1;
//const int delta = CParams::instance()->getParam2i();;
const int minArea = 30;
const double maxAreaRatio = 0.05;
Ptr<MSER2> mser;
mser = MSER2::create(delta, minArea, int(maxAreaRatio * imageArea));
mser->detectRegions(image, all_contours.at(0), all_boxes.at(0), all_contours.at(1), all_boxes.at(1));//得到蓝色车牌和黄色车牌的MSER区域
// mser detect
// color_index = 0 : mser-, detect white characters, which is in blue plate.
// color_index = 1 : mser+, detect dark characters, which is in yellow plate.
#pragma omp parallel for
for (int color_index = 0; color_index < 2; color_index++) {
Color the_color = flags.at(color_index);
std::vector<CCharacter> charVec;
charVec.reserve(128);
match.at(color_index) = Mat::zeros(image.rows, image.cols, image.type());
Mat result = image.clone();
cvtColor(result, result, COLOR_GRAY2BGR);
size_t size = all_contours.at(color_index).size();
int char_index = 0;
int char_size = 20;
// Chinese plate has max 7 characters.
const int char_max_count = 7;
/验证字符大小和输出到rects
for (size_t index = 0; index < size; index++) {
Rect rect = all_boxes.at(color_index)[index];
std::vector<Point>& contour = all_contours.at(color_index)[index];
// 有时一个车牌可能是一个mser rect,所以我们也可以使用mser算法找到车牌
if (usePlateMser) {
RotatedRect rrect = minAreaRect(Mat(contour));
if (verifyRotatedPlateSizes(rrect)) {
//rotatedRectangle(result, rrect, Scalar(255, 0, 0), 2);
if (the_color == BLUE) out_plateRRect_blue.push_back(rrect);
if (the_color == YELLOW) out_plateRRect_yellow.push_back(rrect);
}
}
// find character
if (verifyCharSizes(rect)) {
Mat mserMat = adaptive_image_from_points(contour, rect, Size(char_size, char_size));
Mat charInput = preprocessChar(mserMat, char_size);
Rect charRect = rect;
Point center(charRect.tl().x + charRect.width / 2, charRect.tl().y + charRect.height / 2);
Mat tmpMat;
double ostu_level = cv::threshold(image(charRect), tmpMat, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
//cv::circle(result, center, 3, Scalar(0, 0, 255), 2);
// use judegMDOratio2 function to
// remove the small lines in character like "zh-cuan"
if (judegMDOratio2(image, rect, contour, result)) {
CCharacter charCandidate;
charCandidate.setCharacterPos(charRect);
charCandidate.setCharacterMat(charInput);
charCandidate.setOstuLevel(ostu_level);
charCandidate.setCenterPoint(center);
charCandidate.setIsChinese(false);
charVec.push_back(charCandidate);
}
}
}
//使用矩阵乘法来加速许多样本的分类。 使用概率分数,NMS来减少不太可能是真正的字符的字符,并选择分数大于0.9的强种子
CharsIdentify::instance()->classify(charVec);//charVec是输入ann的车牌候选区域,注意charVec是类CCharacter的一个对象,内容有很多参数,其中就有概率分数这一参数
// NMS来减少不太可能是真正的字符的字符
double overlapThresh = 0.6;
NMStoCharacter(charVec, overlapThresh);
charVec.shrink_to_fit();
std::vector<CCharacter> strongSeedVec; //定义3类种子点
strongSeedVec.reserve(64);
std::vector<CCharacter> weakSeedVec;
weakSeedVec.reserve(64);
std::vector<CCharacter> littleSeedVec;
littleSeedVec.reserve(64);
//size_t charCan_size = charVec.size();
for (auto charCandidate : charVec) {//有点像Python中for的用法
Rect rect = charCandidate.getCharacterPos();
double score = charCandidate.getCharacterScore();
//将charVec中的区域按概率分数填入不同的种子类中
if (charCandidate.getIsStrong()) {
strongSeedVec.push_back(charCandidate);
}
else if (charCandidate.getIsWeak()) {
weakSeedVec.push_back(charCandidate);
//cv::rectangle(result, rect, Scalar(255, 0, 255));
}
else if (charCandidate.getIsLittle()) {
littleSeedVec.push_back(charCandidate);
//cv::rectangle(result, rect, Scalar(255, 0, 255));
}
}
std::vector<CCharacter> searchCandidate = charVec;
// nms保留最强的一个强种子点
overlapThresh = 0.3;
NMStoCharacter(strongSeedVec, overlapThresh);
//使用相似性,将字符合并到一个vector
std::vector<std::vector<CCharacter>> charGroupVec;
charGroupVec.reserve(64);
mergeCharToGroup(strongSeedVec, charGroupVec);
// 根据生成群的行
//通过字符分类器ann得出高概率分数的mser rects无疑可以是一个板块中的字符,我们可以使用这些characeters来拟合一条线,即中间线。
std::vector<CPlate> plateVec;
plateVec.reserve(16);
for (auto charGroup : charGroupVec) {
Rect plateResult = charGroup[0].getCharacterPos();
std::vector<Point> points;
points.reserve(32);
Vec4f line;
int maxarea = 0;
Rect maxrect;
double ostu_level_sum = 0;
int leftx = image.cols;
Point leftPoint(leftx, 0);
int rightx = 0;
Point rightPoint(rightx, 0);
std::vector<CCharacter> mserCharVec;
mserCharVec.reserve(32);
// remove outlier CharGroup
std::vector<CCharacter> roCharGroup;
roCharGroup.reserve(32);
removeRightOutliers(charGroup, roCharGroup, 0.2, 0.5, result);
//roCharGroup = charGroup;
for (auto character : roCharGroup) {
Rect charRect = character.getCharacterPos();
cv::rectangle(result, charRect, Scalar(0, 255, 0), 1);
plateResult |= charRect;
Point center(charRect.tl().x + charRect.width / 2, charRect.tl().y + charRect.height / 2);
points.push_back(center);
mserCharVec.push_back(character);
//cv::circle(result, center, 3, Scalar(0, 255, 0), 2);
ostu_level_sum += character.getOstuLevel();
if (charRect.area() > maxarea) {
maxrect = charRect;
maxarea = charRect.area();
}
if (center.x < leftPoint.x) {
leftPoint = center;
}
if (center.x > rightPoint.x) {
rightPoint = center;
}
}
double ostu_level_avg = ostu_level_sum / (double)roCharGroup.size();
if (1 && showDebug) {
std::cout << "ostu_level_avg:" << ostu_level_avg << std::endl;
}
float ratio_maxrect = (float)maxrect.width / (float)maxrect.height;
if (points.size() >= 2 && ratio_maxrect >= 0.3) {
fitLine(Mat(points), line, CV_DIST_L2, 0, 0.01, 0.01);
float k = line[1] / line[0];
//float angle = atan(k) * 180 / (float)CV_PI;
//std::cout << "k:" << k << std::endl;
//std::cout << "angle:" << angle << std::endl;
//std::cout << "cos:" << 0.3 * cos(k) << std::endl;
//std::cout << "ratio_maxrect:" << ratio_maxrect << std::endl;
std::sort(mserCharVec.begin(), mserCharVec.end(),
[](const CCharacter& r1, const CCharacter& r2) {
return r1.getCharacterPos().tl().x < r2.getCharacterPos().tl().x;
});
CCharacter midChar = mserCharVec.at(int(mserCharVec.size() / 2.f));
Rect midRect = midChar.getCharacterPos();
Point midCenter(midRect.tl().x + midRect.width / 2, midRect.tl().y + midRect.height / 2);
int mindist = 7 * maxrect.width;
std::vector<Vec2i> distVecVec;
distVecVec.reserve(32);
Vec2i mindistVec;
Vec2i avgdistVec;
// 计算dist,这是车牌中两个相近字符之间的距离,使用dist我们可以判断如何计算最大搜索范
//围,并在接下来的步骤中选择滑动窗口的最佳位置。
for (size_t mser_i = 0; mser_i + 1 < mserCharVec.size(); mser_i++) {
Rect charRect = mserCharVec.at(mser_i).getCharacterPos();
Point center(charRect.tl().x + charRect.width / 2, charRect.tl().y + charRect.height / 2);
Rect charRectCompare = mserCharVec.at(mser_i + 1).getCharacterPos();
Point centerCompare(charRectCompare.tl().x + charRectCompare.width / 2,
charRectCompare.tl().y + charRectCompare.height / 2);
int dist = charRectCompare.x - charRect.x;
Vec2i distVec(charRectCompare.x - charRect.x, charRectCompare.y - charRect.y);
distVecVec.push_back(distVec);
//if (dist < mindist) {
// mindist = dist;
// mindistVec = distVec;
//}
}
std::sort(distVecVec.begin(), distVecVec.end(),
[](const Vec2i& r1, const Vec2i& r2) {
return r1[0] < r2[0];
});
avgdistVec = distVecVec.at(int((distVecVec.size() - 1) / 2.f));
//float step = 10.f * (float)maxrect.width;
//float step = (float)mindistVec[0];
float step = (float)avgdistVec[0];
//cv::line(result, Point2f(line[2] - step, line[3] - k*step), Point2f(line[2] + step, k*step + line[3]), Scalar(255, 255, 255));
cv::line(result, Point2f(midCenter.x - step, midCenter.y - k*step), Point2f(midCenter.x + step, k*step + midCenter.y), Scalar(255, 255, 255));
//cv::circle(result, leftPoint, 3, Scalar(0, 0, 255), 2);
CPlate plate;
plate.setPlateLeftPoint(leftPoint);
plate.setPlateRightPoint(rightPoint);
plate.setPlateLine(line);
plate.setPlatDistVec(avgdistVec);
plate.setOstuLevel(ostu_level_avg);
plate.setPlateMergeCharRect(plateResult);
plate.setPlateMaxCharRect(maxrect);
plate.setMserCharacter(mserCharVec);
plateVec.push_back(plate);
}
}
// 使用强种子构建车牌的第一个形状,然后我们需要找到弱种子的字符。
// 因为我们用强壮的种子来建造车牌的中间线,我们可以简单地用这个,考虑弱种子只躺在中线的近处
for (auto plate : plateVec) {
Vec4f line = plate.getPlateLine();
Point leftPoint = plate.getPlateLeftPoint();
Point rightPoint = plate.getPlateRightPoint();
Rect plateResult = plate.getPlateMergeCharRect();
Rect maxrect = plate.getPlateMaxCharRect();
Vec2i dist = plate.getPlateDistVec();
double ostu_level = plate.getOstuLevel();
std::vector<CCharacter> mserCharacter = plate.getCopyOfMserCharacters();
mserCharacter.reserve(16);
float k = line[1] / line[0];
float x_1 = line[2];
float y_1 = line[3];
std::vector<CCharacter> searchWeakSeedVec;
searchWeakSeedVec.reserve(16);
std::vector<CCharacter> searchRightWeakSeed;
searchRightWeakSeed.reserve(8);
std::vector<CCharacter> searchLeftWeakSeed;
searchLeftWeakSeed.reserve(8);
std::vector<CCharacter> slideRightWindow;
slideRightWindow.reserve(8);
std::vector<CCharacter> slideLeftWindow;
slideLeftWindow.reserve(8);
// draw weak seed and little seed from line;从线上吸取弱种子和小种子;
// search for mser rect搜索mser rect
if (1 && showDebug) {
std::cout << "search for mser rect:" << std::endl;
}
if (0 && showDebug) {
std::stringstream ss(std::stringstream::in | std::stringstream::out);
ss << "resources/image/tmp/" << img_index << "_1_" << "searcgMserRect.jpg";
imwrite(ss.str(), result);
}
if (1 && showDebug) {
std::cout << "mserCharacter:" << mserCharacter.size() << std::endl;
}
// 如果强种子的数量大于最大数量,我们不需要所有后续步骤,
//如果没有,我们首先需要在与强种子相同的行中搜索弱种子。
// 判断条件包含强种子与弱种子之间的距离,以及相互之间的相似性,以改善种子生长算法的鲁棒性。
if (mserCharacter.size() < char_max_count) {
double thresh1 = 0.15;
double thresh2 = 2.0;
searchWeakSeed(searchCandidate, searchRightWeakSeed, thresh1, thresh2, line, rightPoint,
maxrect, plateResult, result, CharSearchDirection::RIGHT);
if (1 && showDebug) {
std::cout << "searchRightWeakSeed:" << searchRightWeakSeed.size() << std::endl;
}
for (auto seed : searchRightWeakSeed) {
cv::rectangle(result, seed.getCharacterPos(), Scalar(255, 0, 0), 1);
mserCharacter.push_back(seed);
}
searchWeakSeed(searchCandidate, searchLeftWeakSeed, thresh1, thresh2, line, leftPoint,
maxrect, plateResult, result, CharSearchDirection::LEFT);
if (1 && showDebug) {
std::cout << "searchLeftWeakSeed:" << searchLeftWeakSeed.size() << std::endl;
}
for (auto seed : searchLeftWeakSeed) {
cv::rectangle(result, seed.getCharacterPos(), Scalar(255, 0, 0), 1);
mserCharacter.push_back(seed);
}
}
// 有时弱种子在强种子的中间。 有时两个强壮的种子实际上是一个字符的两个部分。
// 因为我们只考虑强种子左右方向的弱种子。 现在我们检查所有强种子和弱种子。
//不仅要在中间找到种子,还要将两个种子(一个字符的两个部分)组合成一个种子。
//只有通过这个过程,我们才能使用种子计数作为判断是否使用滑动窗口的条件。
float min_thresh = 0.3f;
float max_thresh = 2.5f;
reFoundAndCombineRect(mserCharacter, min_thresh, max_thresh, dist, maxrect, result);
// 如果字符数小于最大计数,则表示行中的mser rect不够。 有时仍然有一些字符无法通过mser算法捕获,如模糊,弱光和一些像zh-cuan这样的汉字。
//为了解决这个问题,我们使用一个简单的滑动窗口方法来查找它们。
if (mserCharacter.size() < char_max_count) {
if (1 && showDebug) {
std::cout << "search chinese:" << std::endl;
std::cout << "judege the left is chinese:" << std::endl;
}
// 如果最左边的字符是中文,这意味着必须是中文版中的第一个字符,
//我们不需要向左移动一个幻灯片窗口。 所以,首先要判断左边的charcater是不是中文。
bool leftIsChinese = false;
if (1) {
std::sort(mserCharacter.begin(), mserCharacter.end(),
[](const CCharacter& r1, const CCharacter& r2) {
return r1.getCharacterPos().tl().x < r2.getCharacterPos().tl().x;
});
CCharacter leftChar = mserCharacter[0];
//Rect theRect = adaptive_charrect_from_rect(leftChar.getCharacterPos(), image.cols, image.rows);
Rect theRect = leftChar.getCharacterPos();
//cv::rectangle(result, theRect, Scalar(255, 0, 0), 1);
Mat region = image(theRect);
Mat binary_region;
ostu_level = cv::threshold(region, binary_region, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
if (1 && showDebug) {
std::cout << "left : ostu_level:" << ostu_level << std::endl;
}
//plate.setOstuLevel(ostu_level);
Mat charInput = preprocessChar(binary_region, char_size);
if (0 /*&& showDebug*/) {
imshow("charInput", charInput);
waitKey(0);
destroyWindow("charInput");
}
std::string label = "";
float maxVal = -2.f;
leftIsChinese = CharsIdentify::instance()->isCharacter(charInput, label, maxVal, true);
//auto character = CharsIdentify::instance()->identifyChinese(charInput, maxVal, leftIsChinese);
//label = character.second;
if (0 /* && showDebug*/) {
std::cout << "isChinese:" << leftIsChinese << std::endl;
std::cout << "chinese:" << label;
std::cout << "__score:" << maxVal << std::endl;
}
}
// 如果最左边的字符不是中文,这意味着我们需要滑动一个窗口来找到错过的mser rect。
// search for sliding window搜索滑动窗口
float ratioWindow = 0.4f;
//float ratioWindow = CParams::instance()->getParam3f();
float threshIsCharacter = 0.8f;
//float threshIsCharacter = CParams::instance()->getParam3f();
if (!leftIsChinese) {
slideWindowSearch(image, slideLeftWindow, line, leftPoint, dist, ostu_level, ratioWindow, threshIsCharacter,
maxrect, plateResult, CharSearchDirection::LEFT, true, result);
if (1 && showDebug) {
std::cout << "slideLeftWindow:" << slideLeftWindow.size() << std::endl;
}
for (auto window : slideLeftWindow) {
cv::rectangle(result, window.getCharacterPos(), Scalar(0, 0, 255), 1);
mserCharacter.push_back(window);
}
}
}
// 如果我们仍然有少于最大计数字符,我们需要向右滑动一个窗口来搜索错过的mser rect。
if (mserCharacter.size() < char_max_count) {
// change ostu_level
float ratioWindow = 0.4f;
//float ratioWindow = CParams::instance()->getParam3f();
float threshIsCharacter = 0.8f;
//float threshIsCharacter = CParams::instance()->getParam3f();
slideWindowSearch(image, slideRightWindow, line, rightPoint, dist, plate.getOstuLevel(), ratioWindow, threshIsCharacter,
maxrect, plateResult, CharSearchDirection::RIGHT, false, result);
if (1 && showDebug) {
std::cout << "slideRightWindow:" << slideRightWindow.size() << std::endl;
}
for (auto window : slideRightWindow) {
cv::rectangle(result, window.getCharacterPos(), Scalar(0, 0, 255), 1);
mserCharacter.push_back(window);
}
}
// computer the plate angle
float angle = atan(k) * 180 / (float)CV_PI;
if (1 && showDebug) {
std::cout << "k:" << k << std::endl;
std::cout << "angle:" << angle << std::endl;
}
// the plateResult rect 需要放大才能包含所有的板块,
// not only the character area.
float widthEnlargeRatio = 1.15f;
float heightEnlargeRatio = 1.25f;
RotatedRect platePos(Point2f((float)plateResult.x + plateResult.width / 2.f, (float)plateResult.y + plateResult.height / 2.f),
Size2f(plateResult.width * widthEnlargeRatio, maxrect.height * heightEnlargeRatio), angle);
// justify the size is likely to be a plate size.
if (verifyRotatedPlateSizes(platePos)) {
rotatedRectangle(result, platePos, Scalar(0, 0, 255), 1);
plate.setPlatePos(platePos);
plate.setPlateColor(the_color);
plate.setPlateLocateType(CMSER);
if (the_color == BLUE) out_plateVec_blue.push_back(plate);
if (the_color == YELLOW) out_plateVec_yellow.push_back(plate);
}
// use deskew to rotate the image, so we need the binary image.
if (1) {
for (auto mserChar : mserCharacter) {
Rect rect = mserChar.getCharacterPos();
match.at(color_index)(rect) = 255;
}
cv::line(match.at(color_index), rightPoint, leftPoint, Scalar(255));
}
}
if (0 /*&& showDebug*/) {
imshow("result", result);
waitKey(0);
destroyWindow("result");
}
if (0) {
imshow("match", match.at(color_index));
waitKey(0);
destroyWindow("match");
}
if (0) {
std::stringstream ss(std::stringstream::in | std::stringstream::out);
ss << "resources/image/tmp/plateDetect/plate_" << img_index << "_" << the_color << ".jpg";
imwrite(ss.str(), result);
}
}
}