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 = _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 函數用於中文字符判斷,后面字符識別時詳細介紹。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM