1. 程式人生 > >EasyPR源碼剖析(8):字符分割

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

resize border 特殊 opened sta sea adapt warp urn

通過前面的學習,我們已經可以從圖像中定位出車牌區域,並且通過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 函數用於中文字符判斷,後面字符識別時詳細介紹。

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