首页 > 代码库 > EasyPR源码剖析(8):字符分割

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 }
View Code

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

  • 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 =http://www.mamicode.com/ _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 }
View Code

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 }
View Code

清除铆钉对字符识别的影响,基本思路是:依次扫描各行,判断跳变的次数,字符所在行跳变次数会很多,但是铆钉所在行则偏少,将每行中跳变次数少于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 }
View Code

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

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 }
View Code

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 }
View Code

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 }
View Code

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

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 }
View Code

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

技术分享     转化为    技术分享

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

 

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