zoukankan      html  css  js  c++  java
  • 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 函数用于中文字符判断,后面字符识别时详细介绍。

  • 相关阅读:
    字符串类型
    数据类型之整型
    数据类型
    两个版本的区别
    变量
    DHCP
    MySQL数据库编译及入门
    NFS网络文件系统
    Rsync 数据同步
    互联网数据分享平台
  • 原文地址:https://www.cnblogs.com/freedomker/p/7280739.html
Copyright © 2011-2022 走看看