首页 > 代码库 > 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 }
下面我们最该字符分割函数中的主要函数进行一个简单的梳理:
- 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 }
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 函数用于中文字符判断,后面字符识别时详细介绍。
EasyPR源码剖析(8):字符分割