EasyPR源码剖析(8):字符分割

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

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

 1 int CCharsSegment::charsSegment(Mat input, vector<Mat>& resultVec, Color color) {
 2   if (!input.data) return 0x01;
 3   Color plateType = color;
 4   Mat input_grey;
 5   cvtColor(input, input_grey, CV_BGR2GRAY);
 6   Mat img_threshold;
 7
 8   img_threshold = input_grey.clone();
 9   spatial_ostu(img_threshold, 8, 2, plateType);
10
11   //车牌铆钉 水平线
12   if (!clearLiuDing(img_threshold)) return 0x02;
13
14   Mat img_contours;
15   img_threshold.copyTo(img_contours);
16
17   vector<vector<Point> > contours;
18   findContours(img_contours,
19                contours,               // a vector of contours
20                CV_RETR_EXTERNAL,       // retrieve the external contours
21                CV_CHAIN_APPROX_NONE);  // all pixels of each contours
22
23   vector<vector<Point> >::iterator itc = contours.begin();
24   vector<Rect> vecRect;
25
26   while (itc != contours.end()) {
27     Rect mr = boundingRect(Mat(*itc));
28     Mat auxRoi(img_threshold, mr);
29
30     if (verifyCharSizes(auxRoi)) vecRect.push_back(mr);
31     ++itc;
32   }
33
34
35   if (vecRect.size() == 0) return 0x03;
36
37   vector<Rect> sortedRect(vecRect);
38   std::sort(sortedRect.begin(), sortedRect.end(),
39             [](const Rect& r1, const Rect& r2) { return r1.x < r2.x; });
40
41   size_t specIndex = 0;
42
43   specIndex = GetSpecificRect(sortedRect);
44
45   Rect chineseRect;
46   if (specIndex < sortedRect.size())
47     chineseRect = GetChineseRect(sortedRect[specIndex]);
48   else
49     return 0x04;
50
51   vector<Rect> newSortedRect;
52   newSortedRect.push_back(chineseRect);
53   RebuildRect(sortedRect, newSortedRect, specIndex);
54
55   if (newSortedRect.size() == 0) return 0x05;
56
57   bool useSlideWindow = true;
58   bool useAdapThreshold = true;
59
60   for (size_t i = 0; i < newSortedRect.size(); i++) {
61     Rect mr = newSortedRect[i];
62
63     // Mat auxRoi(img_threshold, mr);
64     Mat auxRoi(input_grey, mr);
65     Mat newRoi;
66
67     if (i == 0) {
68       if (useSlideWindow) {
69         float slideLengthRatio = 0.1f;
70         if (!slideChineseWindow(input_grey, mr, newRoi, plateType, slideLengthRatio, useAdapThreshold))
71           judgeChinese(auxRoi, newRoi, plateType);
72       }
73       else
74         judgeChinese(auxRoi, newRoi, plateType);
75     }
76     else {
77       if (BLUE == plateType) {
78         threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU);
79       }
80       else if (YELLOW == plateType) {
81         threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU);
82       }
83       else if (WHITE == plateType) {
84         threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
85       }
86       else {
87         threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
88       }
89
90       newRoi = preprocessChar(newRoi);
91     }
92     resultVec.push_back(newRoi);
93   }
94
95   return 0;
96 }

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

  • 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 函数代码如下:

 1 // this spatial_ostu algorithm are robust to
 2 // the plate which has the same light shine, which is that
 3 // the light in the left of the plate is strong than the right.
 4 void spatial_ostu(InputArray _src, int grid_x, int grid_y, Color type) {
 5   Mat src = _src.getMat();
 6
 7   int width = src.cols / grid_x;
 8   int height = src.rows / grid_y;
 9
10   // iterate through grid
11   for (int i = 0; i < grid_y; i++) {
12     for (int j = 0; j < grid_x; j++) {
13       Mat src_cell = Mat(src, Range(i*height, (i + 1)*height), Range(j*width, (j + 1)*width));
14       if (type == BLUE) {
15         cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
16       }
17       else if (type == YELLOW) {
18         cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
19       }
20       else if (type == WHITE) {
21         cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
22       }
23       else {
24         cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
25       }
26     }
27   }
28 }

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

clearLiuDing函数代码如下:

 1 bool clearLiuDing(Mat &img) {
 2   std::vector<float> fJump;
 3   int whiteCount = 0;
 4   const int x = 7;
 5   Mat jump = Mat::zeros(1, img.rows, CV_32F);
 6   for (int i = 0; i < img.rows; i++) {
 7     int jumpCount = 0;
 8
 9     for (int j = 0; j < img.cols - 1; j++) {
10       if (img.at<char>(i, j) != img.at<char>(i, j + 1)) jumpCount++;
11
12       if (img.at<uchar>(i, j) == 255) {
13         whiteCount++;
14       }
15     }
16
17     jump.at<float>(i) = (float) jumpCount;
18   }
19
20   int iCount = 0;
21   for (int i = 0; i < img.rows; i++) {
22     fJump.push_back(jump.at<float>(i));
23     if (jump.at<float>(i) >= 16 && jump.at<float>(i) <= 45) {
24
25       // jump condition
26       iCount++;
27     }
28   }
29
30   // if not is not plate
31   if (iCount * 1.0 / img.rows <= 0.40) {
32     return false;
33   }
34
35   if (whiteCount * 1.0 / (img.rows * img.cols) < 0.15 ||
36       whiteCount * 1.0 / (img.rows * img.cols) > 0.50) {
37     return false;
38   }
39
40   for (int i = 0; i < img.rows; i++) {
41     if (jump.at<float>(i) <= x) {
42       for (int j = 0; j < img.cols; j++) {
43         img.at<char>(i, j) = 0;
44       }
45     }
46   }
47   return true;
48 }

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

verifyCharSizes函数代码如下:

 1 bool CCharsSegment::verifyCharSizes(Mat r) {
 2   // Char sizes 45x90
 3   float aspect = 45.0f / 90.0f;
 4   float charAspect = (float)r.cols / (float)r.rows;
 5   float error = 0.7f;
 6   float minHeight = 10.f;
 7   float maxHeight = 35.f;
 8   // We have a different aspect ratio for number 1, and it can be ~0.2
 9   float minAspect = 0.05f;
10   float maxAspect = aspect + aspect * error;
11   // area of pixels
12   int area = cv::countNonZero(r);
13   // bb area
14   int bbArea = r.cols * r.rows;
15   //% of pixel in area
16   int percPixels = area / bbArea;
17
18   if (percPixels <= 1 && charAspect > minAspect && charAspect < maxAspect &&
19       r.rows >= minHeight && r.rows < maxHeight)
20     return true;
21   else
22     return false;
23 }

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

GetSpecificRect 函数代码如下:

 1 int CCharsSegment::GetSpecificRect(const vector<Rect>& vecRect) {
 2   vector<int> xpositions;
 3   int maxHeight = 0;
 4   int maxWidth = 0;
 5
 6   for (size_t i = 0; i < vecRect.size(); i++) {
 7     xpositions.push_back(vecRect[i].x);
 8
 9     if (vecRect[i].height > maxHeight) {
10       maxHeight = vecRect[i].height;
11     }
12     if (vecRect[i].width > maxWidth) {
13       maxWidth = vecRect[i].width;
14     }
15   }
16
17   int specIndex = 0;
18   for (size_t i = 0; i < vecRect.size(); i++) {
19     Rect mr = vecRect[i];
20     int midx = mr.x + mr.width / 2;
21
22     // use known knowledage to find the specific character
23     // position in 1/7 and 2/7
24     if ((mr.width > maxWidth * 0.8 || mr.height > maxHeight * 0.8) &&
25         (midx < int(m_theMatWidth / 7) * 2 &&
26          midx > int(m_theMatWidth / 7) * 1)) {
27       specIndex = i;
28     }
29   }
30
31   return specIndex;
32 }

GetChineseRect函数代码如下:

 1 Rect CCharsSegment::GetChineseRect(const Rect rectSpe) {
 2   int height = rectSpe.height;
 3   float newwidth = rectSpe.width * 1.15f;
 4   int x = rectSpe.x;
 5   int y = rectSpe.y;
 6
 7   int newx = x - int(newwidth * 1.15);
 8   newx = newx > 0 ? newx : 0;
 9
10   Rect a(newx, y, int(newwidth), height);
11
12   return a;
13 }

slideChineseWindow函数代码如下:

 1 bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float slideLengthRatio, bool useAdapThreshold) {
 2   std::vector<CCharacter> charCandidateVec;
 3
 4   Rect maxrect = mr;
 5   Point tlPoint = mr.tl();
 6
 7   bool isChinese = true;
 8   int slideLength = int(slideLengthRatio * maxrect.width);
 9   int slideStep = 1;
10   int fromX = 0;
11   fromX = tlPoint.x;
12
13   for (int slideX = -slideLength; slideX < slideLength; slideX += slideStep) {
14     float x_slide = 0;
15
16     x_slide = float(fromX + slideX);
17
18     float y_slide = (float)tlPoint.y;
19     Point2f p_slide(x_slide, y_slide);
20
21     //cv::circle(image, p_slide, 2, Scalar(255), 1);
22
23     int chineseWidth = int(maxrect.width);
24     int chineseHeight = int(maxrect.height);
25
26     Rect rect(Point2f(x_slide, y_slide), Size(chineseWidth, chineseHeight));
27
28     if (rect.tl().x < 0 || rect.tl().y < 0 || rect.br().x >= image.cols || rect.br().y >= image.rows)
29       continue;
30
31     Mat auxRoi = image(rect);
32
33     Mat roiOstu, roiAdap;
34     if (1) {
35       if (BLUE == plateType) {
36         threshold(auxRoi, roiOstu, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU);
37       }
38       else if (YELLOW == plateType) {
39         threshold(auxRoi, roiOstu, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU);
40       }
41       else if (WHITE == plateType) {
42         threshold(auxRoi, roiOstu, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU);
43       }
44       else {
45         threshold(auxRoi, roiOstu, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
46       }
47       roiOstu = preprocessChar(roiOstu, kChineseSize);
48
49       CCharacter charCandidateOstu;
50       charCandidateOstu.setCharacterPos(rect);
51       charCandidateOstu.setCharacterMat(roiOstu);
52       charCandidateOstu.setIsChinese(isChinese);
53       charCandidateVec.push_back(charCandidateOstu);
54     }
55     if (useAdapThreshold) {
56       if (BLUE == plateType) {
57         adaptiveThreshold(auxRoi, roiAdap, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, 0);
58       }
59       else if (YELLOW == plateType) {
60         adaptiveThreshold(auxRoi, roiAdap, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV, 3, 0);
61       }
62       else if (WHITE == plateType) {
63         adaptiveThreshold(auxRoi, roiAdap, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV, 3, 0);
64       }
65       else {
66         adaptiveThreshold(auxRoi, roiAdap, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, 0);
67       }
68       roiAdap = preprocessChar(roiAdap, kChineseSize);
69
70       CCharacter charCandidateAdap;
71       charCandidateAdap.setCharacterPos(rect);
72       charCandidateAdap.setCharacterMat(roiAdap);
73       charCandidateAdap.setIsChinese(isChinese);
74       charCandidateVec.push_back(charCandidateAdap);
75     }
76
77   }
78
79   CharsIdentify::instance()->classifyChinese(charCandidateVec);
80
81   double overlapThresh = 0.1;
82   NMStoCharacter(charCandidateVec, overlapThresh);
83
84   if (charCandidateVec.size() >= 1) {
85     std::sort(charCandidateVec.begin(), charCandidateVec.end(),
86       [](const CCharacter& r1, const CCharacter& r2) {
87       return r1.getCharacterScore() > r2.getCharacterScore();
88     });
89
90     newRoi = charCandidateVec.at(0).getCharacterMat();
91     return true;
92   }
93
94   return false;
95
96 }

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

preprocessChar函数代码如下:

 1 Mat preprocessChar(Mat in, int char_size) {
 2   // Remap image
 3   int h = in.rows;
 4   int w = in.cols;
 5
 6   int charSize = char_size;
 7
 8   Mat transformMat = Mat::eye(2, 3, CV_32F);
 9   int m = max(w, h);
10   transformMat.at<float>(0, 2) = float(m / 2 - w / 2);
11   transformMat.at<float>(1, 2) = float(m / 2 - h / 2);
12
13   Mat warpImage(m, m, in.type());
14   warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR,
15     BORDER_CONSTANT, Scalar(0));
16
17   Mat out;
18   cv::resize(warpImage, out, Size(charSize, charSize));
19
20   return out;
21 }

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

     转化为   

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

时间: 2024-10-28 18:56:33

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

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

一.LBP特征 LBP指局部二值模式,英文全称:Local Binary Pattern,是一种用来描述图像局部特征的算子,LBP特征具有灰度不变性和旋转不变性等显著优点. 原始的LBP算子定义在像素3*3的邻域内,以邻域中心像素为阈值,相邻的8个像素的灰度值与邻域中心的像素值进行比较,若周围像素大于中心像素值,则该像素点的位置被标记为1,否则为0.这样,3*3邻域内的8个点经过比较可产生8位二进制数,将这8位二进制数依次排列形成一个二进制数字,这个二进制数字就是中心像素的LBP值,LBP值共有

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

一.简介 通过颜色定位和Sobel算子定位可以计算出一个个的矩形区域,这些区域都是潜在车牌区域,但是在进行SVM判别是否是车牌之前,还需要进行一定的处理.主要是考虑到以下几个问题: 1.定位区域存在一定程度的倾斜,需要旋转到正常视角: 2.定位区域存在偏斜,除了进行旋转之后,还需要进行仿射变换: 3.定位出区域的大小不一致,需要对车牌的尺寸进行统一. 仿射变换(Affine Transformation 或 Affine Map),又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一

《python源码剖析》笔记 Python虚拟机框架

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1. Python虚拟机会从编译得到的PyCodeObject对象中依次读入每一条字节码指令, 并在当前的上下文环境中执行这条字节码指令. Python虚拟机实际上是在模拟操作中执行文件的过程 PyCodeObject对象中包含了字节码指令以及程序的所有静态信息,但没有包含 程序运行时的动态信息--执行环境(PyFrameObject) 2.Python源码中的PyFrameObject

GDAL源码剖析(一)(转载)

GDAL源码剖析(一) GDAL 前言:一直在使用和研究GDAL的相关东西,发现网上对GDAL的内容倒是不少,但是很少有系统的介绍说明,以及内部的一些结构说明,基于这些原因,将本人的一些粗浅的理解放在此处,形成一个系列,暂时名为<GDAL源码剖析>(名称有点大言不惭,欢迎大家口水吐之,板砖拍之),供大家交流参考,有什么错误之处,望大家不吝指正,本系列对于GDAL的使用均是在Windows平台下,对于Linux平台下的不在此系列讨论范围之内.此外,转载本博客内容,请注明出处,强烈鄙视转载后不注明

strlen源码剖析(可查看glibc和VC的CRT源代码)

学习高效编程的有效途径之一就是阅读高手写的源代码,CRT(C/C++ Runtime Library)作为底层的函数库,实现必然高效.恰好手中就有glibc和VC的CRT源代码,于是挑了一个相对简单的函数strlen研究了一下,并对各种实现作了简单的效率测试. strlen的函数原形如下: size_t strlen(const char *str); strlen返回str中字符的个数,其中str为一个以'\0'结尾的字符串(a null-terminated string). 1. 简单实现

Python源码剖析笔记0 ——C语言基础

python源码剖析笔记0--C语言基础回顾 要分析python源码,C语言的基础不能少,特别是指针和结构体等知识.这篇文章先回顾C语言基础,方便后续代码的阅读. 1 关于ELF文件 linux中的C编译得到的目标文件和可执行文件都是ELF格式的,可执行文件中以segment来划分,目标文件中,我们是以section划分.一个segment包含一个或多个section,通过readelf命令可以看到完整的section和segment信息.看一个栗子: char pear[40]; static

STL 源码剖析 算法 stl_algo.h -- nth_element

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie nth_element ------------------------------------------------------------------------------ 描述:重新排序,使得[nth,last)内没有任何一个元素小于[first,nth)内的元素, 但对于[first,nth)和[nth,last)两个子区间内的元素次序则无任何保证. 思路: 1.以 media

STL 源码剖析 算法 stl_algo.h -- merge sort

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie merge sort ---------------------------------------------------------------------- 描述:归并排序 思路: 1.将区间对半分割 2.对左.右段分别排序 3.利用inplace_merge将左.右段合并成为一个完整的有序序列 复杂度:O(nlog n) 源码: template<class Bidirection

Python源码剖析笔记4-内建数据类型

Python源码剖析笔记4-内建数据类型 Python内建数据类型包括整数对象PyIntObject,字符串对象PyStringObject,列表对象PyListObject以及字典对象PyDictObject等.整数对象之前已经分析过了,这一篇文章准备分析下余下几个对象,这次在<python源码剖析>中已经写的很详细的部分就不赘述了,主要是总结一些之前看书时疑惑的地方. 1 整数对象-PyIntObject 参见 python整数对象. 2 字符串对象-PyStringObject 2.1