通过前面的学习,我们已经可以从图像中定位出车牌区域,并且通过SVM模型删除“虚假”车牌,下面我们需要对车牌检测步骤中获取到的车牌图像,进行光学字符识别(OCR),在进行光学字符识别之前,需要对车牌图块进行灰度化,二值化,然后使用一系列算法获取到车牌的每个字符的分割图块。本节主要对该字符分割部分进行详细讨论。

EasyPR中,字符分割部分主要是在类 CCharsSegment 中进行的,字符分割函数为 charsSegment()

 int CCharsSegment::charsSegment(Mat input, vector<Mat>& resultVec, Color color) {
if (!input.data) return 0x01;
Color plateType = color;
Mat input_grey;
cvtColor(input, input_grey, CV_BGR2GRAY);
Mat img_threshold; img_threshold = input_grey.clone();
spatial_ostu(img_threshold, , , plateType); //车牌铆钉 水平线
if (!clearLiuDing(img_threshold)) return 0x02; Mat img_contours;
img_threshold.copyTo(img_contours); vector<vector<Point> > contours;
findContours(img_contours,
contours, // a vector of contours
CV_RETR_EXTERNAL, // retrieve the external contours
CV_CHAIN_APPROX_NONE); // all pixels of each contours vector<vector<Point> >::iterator itc = contours.begin();
vector<Rect> vecRect; while (itc != contours.end()) {
Rect mr = boundingRect(Mat(*itc));
Mat auxRoi(img_threshold, mr); if (verifyCharSizes(auxRoi)) vecRect.push_back(mr);
++itc;
} if (vecRect.size() == ) return 0x03; vector<Rect> sortedRect(vecRect);
std::sort(sortedRect.begin(), sortedRect.end(),
[](const Rect& r1, const Rect& r2) { return r1.x < r2.x; }); size_t specIndex = ; specIndex = GetSpecificRect(sortedRect); Rect chineseRect;
if (specIndex < sortedRect.size())
chineseRect = GetChineseRect(sortedRect[specIndex]);
else
return 0x04; vector<Rect> newSortedRect;
newSortedRect.push_back(chineseRect);
RebuildRect(sortedRect, newSortedRect, specIndex); if (newSortedRect.size() == ) return 0x05; bool useSlideWindow = true;
bool useAdapThreshold = true; for (size_t i = ; i < newSortedRect.size(); i++) {
Rect mr = newSortedRect[i]; // Mat auxRoi(img_threshold, mr);
Mat auxRoi(input_grey, mr);
Mat newRoi; if (i == ) {
if (useSlideWindow) {
float slideLengthRatio = 0.1f;
if (!slideChineseWindow(input_grey, mr, newRoi, plateType, slideLengthRatio, useAdapThreshold))
judgeChinese(auxRoi, newRoi, plateType);
}
else
judgeChinese(auxRoi, newRoi, plateType);
}
else {
if (BLUE == plateType) {
threshold(auxRoi, newRoi, , , CV_THRESH_BINARY + CV_THRESH_OTSU);
}
else if (YELLOW == plateType) {
threshold(auxRoi, newRoi, , , CV_THRESH_BINARY_INV + CV_THRESH_OTSU);
}
else if (WHITE == plateType) {
threshold(auxRoi, newRoi, , , CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
}
else {
threshold(auxRoi, newRoi, , , CV_THRESH_OTSU + CV_THRESH_BINARY);
} newRoi = preprocessChar(newRoi);
}
resultVec.push_back(newRoi);
} return ;
}

下面我们最该字符分割函数中的主要函数进行一个简单的梳理:

  • spatial_ostu 空间otsu算法,主要用于处理光照不均匀的图像,对于当前图像,分块分别进行二值化;
  • clearLiuDing 处理车牌上铆钉和水平线,因为铆钉和字符连在一起,会影响后面识别的精度。此处有一个特别的乌龙事件,就是铆钉的读音应该是maoding,不读liuding;
  • verifyCharSizes 字符大小验证;
  • GetSpecificRect  获取特殊字符的位置,主要是车牌中除汉字外的第一个字符,一般位于车牌的 1/7 ~ 2/7宽度处;
  • GetChineseRect 获取汉字字符,一般为特殊字符左移字符宽度的1.15倍;
  • RebuildRect 从左到右取前7个字符,排除右边边界会出现误判的 I ;
  • slideChineseWindow 改进中文字符的识别,在识别中文时,增加一个小型的滑动窗口,以此弥补通过省份字符直接查找中文字符时的定位不精等现象;
  • preprocessChar 识别字符前预处理,主要是通过仿射变换,将字符的大小变换为20 *20;
  • judgeChinese 中文字符判断,后面字符识别时详细介绍。

spatial_ostu 函数代码如下:

 // this spatial_ostu algorithm are robust to
// the plate which has the same light shine, which is that
// the light in the left of the plate is strong than the right.
void spatial_ostu(InputArray _src, int grid_x, int grid_y, Color type) {
Mat src = _src.getMat(); int width = src.cols / grid_x;
int height = src.rows / grid_y; // iterate through grid
for (int i = ; i < grid_y; i++) {
for (int j = ; j < grid_x; j++) {
Mat src_cell = Mat(src, Range(i*height, (i + )*height), Range(j*width, (j + )*width));
if (type == BLUE) {
cv::threshold(src_cell, src_cell, , , CV_THRESH_OTSU + CV_THRESH_BINARY);
}
else if (type == YELLOW) {
cv::threshold(src_cell, src_cell, , , CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
}
else if (type == WHITE) {
cv::threshold(src_cell, src_cell, , , CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
}
else {
cv::threshold(src_cell, src_cell, , , CV_THRESH_OTSU + CV_THRESH_BINARY);
}
}
}
}

spatial_ostu 函数主要是为了应对左右光照不一致的情况,譬如车牌的左边部分光照比右边部分要强烈的多,通过图像分块处理,提高otsu分割的鲁棒性;

clearLiuDing函数代码如下:

 bool clearLiuDing(Mat &img) {
std::vector<float> fJump;
int whiteCount = ;
const int x = ;
Mat jump = Mat::zeros(, img.rows, CV_32F);
for (int i = ; i < img.rows; i++) {
int jumpCount = ; for (int j = ; j < img.cols - ; j++) {
if (img.at<char>(i, j) != img.at<char>(i, j + )) jumpCount++; if (img.at<uchar>(i, j) == ) {
whiteCount++;
}
} jump.at<float>(i) = (float) jumpCount;
} int iCount = ;
for (int i = ; i < img.rows; i++) {
fJump.push_back(jump.at<float>(i));
if (jump.at<float>(i) >= && jump.at<float>(i) <= ) { // jump condition
iCount++;
}
} // if not is not plate
if (iCount * 1.0 / img.rows <= 0.40) {
return false;
} if (whiteCount * 1.0 / (img.rows * img.cols) < 0.15 ||
whiteCount * 1.0 / (img.rows * img.cols) > 0.50) {
return false;
} for (int i = ; i < img.rows; i++) {
if (jump.at<float>(i) <= x) {
for (int j = ; j < img.cols; j++) {
img.at<char>(i, j) = ;
}
}
}
return true;
}

清除铆钉对字符识别的影响,基本思路是:依次扫描各行,判断跳变的次数,字符所在行跳变次数会很多,但是铆钉所在行则偏少,将每行中跳变次数少于7的行判定为铆钉,清除影响。

verifyCharSizes函数代码如下:

 bool CCharsSegment::verifyCharSizes(Mat r) {
// Char sizes 45x90
float aspect = 45.0f / 90.0f;
float charAspect = (float)r.cols / (float)r.rows;
float error = 0.7f;
float minHeight = .f;
float maxHeight = .f;
// We have a different aspect ratio for number 1, and it can be ~0.2
float minAspect = 0.05f;
float maxAspect = aspect + aspect * error;
// area of pixels
int area = cv::countNonZero(r);
// bb area
int bbArea = r.cols * r.rows;
//% of pixel in area
int percPixels = area / bbArea; if (percPixels <= && charAspect > minAspect && charAspect < maxAspect &&
r.rows >= minHeight && r.rows < maxHeight)
return true;
else
return false;
}

主要是从面积,长宽比和字符的宽度高度等角度进行字符校验。

GetSpecificRect 函数代码如下:

 int CCharsSegment::GetSpecificRect(const vector<Rect>& vecRect) {
vector<int> xpositions;
int maxHeight = ;
int maxWidth = ; for (size_t i = ; i < vecRect.size(); i++) {
xpositions.push_back(vecRect[i].x); if (vecRect[i].height > maxHeight) {
maxHeight = vecRect[i].height;
}
if (vecRect[i].width > maxWidth) {
maxWidth = vecRect[i].width;
}
} int specIndex = ;
for (size_t i = ; i < vecRect.size(); i++) {
Rect mr = vecRect[i];
int midx = mr.x + mr.width / ; // use known knowledage to find the specific character
// position in 1/7 and 2/7
if ((mr.width > maxWidth * 0.8 || mr.height > maxHeight * 0.8) &&
(midx < int(m_theMatWidth / ) * &&
midx > int(m_theMatWidth / ) * )) {
specIndex = i;
}
} return specIndex;
}

GetChineseRect函数代码如下:

 Rect CCharsSegment::GetChineseRect(const Rect rectSpe) {
int height = rectSpe.height;
float newwidth = rectSpe.width * 1.15f;
int x = rectSpe.x;
int y = rectSpe.y; int newx = x - int(newwidth * 1.15);
newx = newx > ? newx : ; Rect a(newx, y, int(newwidth), height); return a;
}

slideChineseWindow函数代码如下:

 bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float slideLengthRatio, bool useAdapThreshold) {
std::vector<CCharacter> charCandidateVec; Rect maxrect = mr;
Point tlPoint = mr.tl(); bool isChinese = true;
int slideLength = int(slideLengthRatio * maxrect.width);
int slideStep = ;
int fromX = ;
fromX = tlPoint.x; for (int slideX = -slideLength; slideX < slideLength; slideX += slideStep) {
float x_slide = ; x_slide = float(fromX + slideX); float y_slide = (float)tlPoint.y;
Point2f p_slide(x_slide, y_slide); //cv::circle(image, p_slide, 2, Scalar(255), 1); int chineseWidth = int(maxrect.width);
int chineseHeight = int(maxrect.height); Rect rect(Point2f(x_slide, y_slide), Size(chineseWidth, chineseHeight)); if (rect.tl().x < || rect.tl().y < || rect.br().x >= image.cols || rect.br().y >= image.rows)
continue; Mat auxRoi = image(rect); Mat roiOstu, roiAdap;
if () {
if (BLUE == plateType) {
threshold(auxRoi, roiOstu, , , CV_THRESH_BINARY + CV_THRESH_OTSU);
}
else if (YELLOW == plateType) {
threshold(auxRoi, roiOstu, , , CV_THRESH_BINARY_INV + CV_THRESH_OTSU);
}
else if (WHITE == plateType) {
threshold(auxRoi, roiOstu, , , CV_THRESH_BINARY_INV + CV_THRESH_OTSU);
}
else {
threshold(auxRoi, roiOstu, , , CV_THRESH_OTSU + CV_THRESH_BINARY);
}
roiOstu = preprocessChar(roiOstu, kChineseSize); CCharacter charCandidateOstu;
charCandidateOstu.setCharacterPos(rect);
charCandidateOstu.setCharacterMat(roiOstu);
charCandidateOstu.setIsChinese(isChinese);
charCandidateVec.push_back(charCandidateOstu);
}
if (useAdapThreshold) {
if (BLUE == plateType) {
adaptiveThreshold(auxRoi, roiAdap, , ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, , );
}
else if (YELLOW == plateType) {
adaptiveThreshold(auxRoi, roiAdap, , ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV, , );
}
else if (WHITE == plateType) {
adaptiveThreshold(auxRoi, roiAdap, , ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV, , );
}
else {
adaptiveThreshold(auxRoi, roiAdap, , ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, , );
}
roiAdap = preprocessChar(roiAdap, kChineseSize); CCharacter charCandidateAdap;
charCandidateAdap.setCharacterPos(rect);
charCandidateAdap.setCharacterMat(roiAdap);
charCandidateAdap.setIsChinese(isChinese);
charCandidateVec.push_back(charCandidateAdap);
} } CharsIdentify::instance()->classifyChinese(charCandidateVec); double overlapThresh = 0.1;
NMStoCharacter(charCandidateVec, overlapThresh); if (charCandidateVec.size() >= ) {
std::sort(charCandidateVec.begin(), charCandidateVec.end(),
[](const CCharacter& r1, const CCharacter& r2) {
return r1.getCharacterScore() > r2.getCharacterScore();
}); newRoi = charCandidateVec.at().getCharacterMat();
return true;
} return false; }

在对中文字符进行识别时,增加一个小型的滑动窗口,以弥补通过省份字符直接查找中文字符时的定位不精等现象。

preprocessChar函数代码如下:

 Mat preprocessChar(Mat in, int char_size) {
// Remap image
int h = in.rows;
int w = in.cols; int charSize = char_size; Mat transformMat = Mat::eye(, , CV_32F);
int m = max(w, h);
transformMat.at<float>(, ) = float(m / - w / );
transformMat.at<float>(, ) = float(m / - h / ); Mat warpImage(m, m, in.type());
warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR,
BORDER_CONSTANT, Scalar()); Mat out;
cv::resize(warpImage, out, Size(charSize, charSize)); return out;
}

首先进行仿射变换,将字符统一大小,并归一化到中间,并resize为 20*20,如下图所示:

     转化为   

judgeChinese 函数用于中文字符判断,后面字符识别时详细介绍。

EasyPR源码剖析(8):字符分割的更多相关文章

  1. EasyPR源码剖析(1):概述

    EasyPR(Easy to do Plate Recognition)是本人在opencv学习过程中接触的一个开源的中文车牌识别系统,项目Git地址为https://github.com/liuru ...

  2. EasyPR源码剖析(9):字符识别

    在上一篇文章的介绍中,我们已经通过相应的字符分割方法,将车牌区域进行分割,得到7个分割字符图块,接下来要做的就是将字符图块放入训练好的神经网络模型,通过模型来预测每个图块所表示的具体字符.神经网络的介 ...

  3. EasyPR源码剖析(4):车牌定位之Sobel算子定位

    一.简介 sobel算子主要是用于获得数字图像的一阶梯度,常见的应用是边缘检测. Ⅰ.水平变化: 将 I 与一个奇数大小的内核进行卷积.比如,当内核大小为3时, 的计算结果为: Ⅱ.垂直变化: 将: ...

  4. EasyPR源码剖析(3):车牌定位之颜色定位

    一.简介 对车牌颜色进行识别,可能大部分人首先想到的是RGB模型, 但是此处RGB模型有一定的局限性,譬如蓝色,其值是255,还需要另外两个分量都为0,不然很有可能你得到的值是白色.黄色更麻烦,它是由 ...

  5. EasyPR源码剖析(7):车牌判断之SVM

    前面的文章中我们主要介绍了车牌定位的相关技术,但是定位出来的相关区域可能并非是真实的车牌区域,EasyPR通过SVM支持向量机,一种机器学习算法来判定截取的图块是否是真的“车牌”,本节主要对相关的技术 ...

  6. EasyPR源码剖析(6):车牌判断之LBP特征

    一.LBP特征 LBP指局部二值模式,英文全称:Local Binary Pattern,是一种用来描述图像局部特征的算子,LBP特征具有灰度不变性和旋转不变性等显著优点. 原始的LBP算子定义在像素 ...

  7. EasyPR源码剖析(5):车牌定位之偏斜扭转

    一.简介 通过颜色定位和Sobel算子定位可以计算出一个个的矩形区域,这些区域都是潜在车牌区域,但是在进行SVM判别是否是车牌之前,还需要进行一定的处理.主要是考虑到以下几个问题: 1.定位区域存在一 ...

  8. EasyPR源码剖析(2):车牌定位

    上一篇主要介绍了车牌识别的整体框架和流程,车牌识别主要划分为了两个过程:即车牌检测和字符识别,而车牌识别的核心环节就是这一节主要介绍的车牌定位,即 Plate Locate.车牌定位主要是将图片中有可 ...

  9. Apache Spark源码剖析

    Apache Spark源码剖析(全面系统介绍Spark源码,提供分析源码的实用技巧和合理的阅读顺序,充分了解Spark的设计思想和运行机理) 许鹏 著   ISBN 978-7-121-25420- ...

随机推荐

  1. 自定义Windows右击菜单调用Winform程序

    U9_Git中ignore文件处理 背景 U9代码中有许多自动生成的文件,不需要上传Git必须BE Entity中的.target文件 .bak 文件 Enum.cs结尾的文件,还有许多 extand ...

  2. (8/24) 图片跳坑大战--css中的图片处理

    前言:此节的开展是在上一节的基础上进行的,(每一节都是从无到有编写关于此节的知识),最后会附上相关完整代码.上一节 CSS中的图片处理 1.新建images文件夹 在src目录下新建一个images文 ...

  3. servlet对象的生命周期

    servlet对象默认第一次访问的时候创建,服务器关闭的时候销毁.当servlet对象创建的时候会调用init方法,当对象销毁的时候,会调用destroy方法,每次访问servlet时,都会调用ser ...

  4. selenium与chrome浏览器及驱动的版本匹配

    用selenium+python+webdriver完成UI功能自动化,经常会碰到浏览器版本与驱动的版本不匹配而引起报错,下面就selenium与chrome浏览器及驱动的版本匹配 做个总结. 使用W ...

  5. java中获取远程ip的一个坑

    发现在高请求量的时候获取hostName慢,后发现getHostName方法慢导致的:需要获取hostName为获取ip的方式了:java 中 InetSocketAddress // remoteA ...

  6. C语言--第01次作业

    分支.顺序结构 1.本章学习总结 1.1思维导图 1.2 本章学习体会及代码量学习体会 1.2.1 学习体会 本周学习了分支.顺序结构,学到的大部分都在思维导图介绍了,不懂的地方例如有switch的运 ...

  7. C 语言能不能在头文件定义全局变量?

    可以,但一般不会将全局变量的定义写在头文件中. 因为如果多个 C 源文件都添加了头文件,很容易引起重定义的问题.这时候一般编译器都会提示:“multiple definition of... firs ...

  8. Nginx之 try_files 指令

    location / { try_files $uri $uri/ /index.php; } 当用户请求 http://localhost/example 时,这里的 $uri 就是 /exampl ...

  9. 【C++】C++中的string类的用法总结

    相信使用过MFC编程的朋友对CString这个类的印象应该非常深刻吧?的确,MFC中的CString类使用起来真的非常的方便好用.但是如果离开了MFC框架,还有没有这样使用起来非常方便的类呢?答案是肯 ...

  10. Java与Netty实现高性能高并发

    摘要: 1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程 ...