通过前面的学习,我们已经可以从图像中定位出车牌区域,并且通过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. html+css常用小笔记(持续更新)

    1 去掉input点击时的蓝色边框 outline:none; 2 禁止文本选中 -webkit-touch-callout: none; /* iOS Safari */ -webkit-user- ...

  2. redis基本数据结构

    redis5中数据类型

  3. Storm实现实时大数据分析(storm介绍,与Hadoop比较,)

    一.storm与Hadoop对比 Hadoop: 全量数据处理使用的大多是鼎鼎大名的hadoop或者hive,作为一个批处理系统,hadoop以其吞吐量大.自动容错等优点,在海量数据处理上得到了广泛的 ...

  4. 彻底搞懂Scrapy的中间件(三)

    在前面两篇文章介绍了下载器中间件的使用,这篇文章将会介绍爬虫中间件(Spider Middleware)的使用. 爬虫中间件 爬虫中间件的用法与下载器中间件非常相似,只是它们的作用对象不同.下载器中间 ...

  5. DP问题

    1.背包问题

  6. CentOS更换镜像源

    使用说明 首先备份/etc/yum.repos.d/CentOS-Base.repo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/Cen ...

  7. nodeJs 使用 express-http-proxy 转发请求

    开发过程中经常需要用到 nodeJs做转发层 使用express配合 express-http-proxy 可以轻松的完成转发 使用过程: 安装 express-http-proxy npm inst ...

  8. Axis2 服务器端抛出ServiceClass object does not implement问题解决方法

    在用eclipse配合Axis2进行开发的时候,编译通过,启动tomcat也顺利,但是就是在调用服务器端的服务时,会抛出: The ServiceClass object does not imple ...

  9. gitkraken clone报错 Configured SSH key is invalid

    gitkraken clone远程仓库时报错 Configured SSH key is invalid. Please confirm that is properly associated wit ...

  10. 不用安装Oracle客户端

    1 pl/sql developer 1.1 下载解压instantclient-basic-nt-12.1.0.2.0. 1.2 在其目录下新建Network/ADMIN/tnsnames.ora文 ...