之前的车牌定位中已经获取到了车牌的位置,并且对车牌进行了提取。我们最终的目的是进行车牌识别,在这之前需要将字符进行分割,方便对每一个字符进行识别,最后将其拼接后便是完整的车牌号码。关于车牌定位可以看这篇文章:OpenCV车牌定位(C++),本文使用的图片也是来自这里。


先来看一看原图:


最左边的汉字本来是 沪,截取时只获得了右边一点点的部分,这与原图和获取方法都有关,对于 川、沪… 这一类左右分开的字会经常发生这类问题,对方法进行优化后可以解决,这里暂时不进行讨论。

后面的字都是完整的,字符分割的过程不会受影响。首先来一波常规操作,为了更方便处理,将其变成灰度图片:


分割的方法不止一种,最简单的就是多加点人工成分,按照大致宽度再微调进行截取,但是这样看似最快其实成本最高,只适用于单一的图片,因此这种容错低且不够自动的方法就不考虑了。

目前我使用了两种不同的方法,一种是进行边缘检测再检测轮廓,根据字符的轮廓特点筛选出字符;另一种就是像素值判断,主要根据像素数量使用水平映射截取宽度,垂直映射因为高度基本一致就不需要了,方法于水平映射一样。

两种方法我都写在后面,根据需要自行复制。如果要使用像素值进行判断的话,就需要再将灰度图转换成二值化图片,使用阈值分割就行了。若使用第一种用轮廓分割的方法,灰度图和二值化图片都可以,结果没什么区别。


检测轮廓进行分割

边缘检测
对图像进行边缘检测,这里采用的是 Canny 边缘检测,处理后的结果如下:


可以看到每个字的边缘都被描绘出来了,接下来就将每个字的轮廓获取出来。

检测轮廓
直接使用 findContours() 将所有轮廓提取出来,再将其在原图中画出来看看效果:


可以看到不仅仅是每个字被框出来了,还有内部以及图像中表现特殊部分的轮廓也有,接下来我们就根据每个字的大致大小筛选出我们想要的结果:


这样看起来是不是就成功了,然后根据轮廓位置将每个字提取出来就行了,不过在这里每个轮廓的前后顺序不一定是图像中的位置,这里我使用每个轮廓左上角横坐标 x 的大小来排序。

完整代码:

  1. #include <iostream>  
  2. #include <opencv2/highgui/highgui.hpp>  
  3. #include <opencv2/imgproc.hpp>
  4. #include <opencv2/imgproc/types_c.h>
  5. #include <map>
  6. using namespace std;
  7. using namespace cv;
  8. int main() {
  9.         Mat img = imread("number.jpg");
  10.         Mat gray_img;
  11.         // 生成灰度图像
  12.         cvtColor(img, gray_img, CV_BGR2GRAY);
  13.         // 高斯模糊
  14.         Mat img_gau;
  15.         GaussianBlur(gray_img, img_gau, Size(3, 3), 0, 0);
  16.         // 阈值分割
  17.         Mat img_seg;
  18.         threshold(img_gau, img_seg, 0, 255, THRESH_BINARY + THRESH_OTSU);
  19.         // 边缘检测,提取轮廓
  20.         Mat img_canny;
  21.         Canny(img_seg, img_canny, 200, 100);
  22.         vector<vector<Point>> contours;
  23.         vector<Vec4i> hierarchy;
  24.         findContours(img_canny, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE, Point());
  25.         int size = (int)(contours.size());
  26.         // 保存符号边框的序号
  27.         vector<int> num_order;
  28.         map<int, int> num_map;
  29.         for (int i = 0; i < size; i++) {
  30.                 // 获取边框数据
  31.                 Rect number_rect = boundingRect(contours[i]);
  32.                 int width = number_rect.width;
  33.                 int height = number_rect.height;
  34.                 // 去除较小的干扰边框,筛选出合适的区域
  35.                 if (width > img.cols/10 && height > img.rows/2) {
  36.                         rectangle(img_seg, number_rect.tl(), number_rect.br(), Scalar(255, 255, 255), 1, 1, 0);
  37.                         num_order.push_back(number_rect.x);
  38.                         num_map[number_rect.x] = i;
  39.                 }
  40.         }
  41.         // 按符号顺序提取
  42.         sort(num_order.begin(), num_order.end());
  43.         for (int i = 0; i < num_order.size(); i++) {
  44.                 Rect number_rect = boundingRect(contours[num_map.find(num_order[i])->second]);
  45.                 Rect choose_rect(number_rect.x, 0, number_rect.width, gray_img.rows);
  46.                 Mat number_img = gray_img(choose_rect);
  47.                 imshow("number" + to_string(i), number_img);
  48.                 // imwrite("number" + to_string(i) + ".jpg", number_img);
  49.         }
  50.         imshow("添加方框", gray_img);
  51.         waitKey(0);
  52.         return 0;
  53. }

像素值判断进行分割

分割方法:首先判断每一列的像素值大于 0 的像素个数超过5个时,认为此列是有数字的,记录每列像素是否大于 5,产生一个数组。

  1. // 确认为 1 的像素
  2.         int pixrow[1000];
  3.         for (int i = 0; i < roi_col - 1; i++) {
  4.                 for (int j = 0; j < roi_row - 1; j++) {
  5.                         pix = img_threadhold.at<uchar>(j, i);
  6.                         pixrow[i] = 0;
  7.                         if (pix > 0) {
  8.                                 pixrow[i] = 1;
  9.                                 break;
  10.                         }
  11.                 }
  12.         }
  13.         // 对数组进行滤波,减少突变概率
  14.         for (int i = 2; i < roi_col - 1 - 2; i++) {
  15.                 if ((pixrow[i - 1] + pixrow[i - 2] + pixrow[i + 1] + pixrow[i + 2]) >= 3) {
  16.                         pixrow[i] = 1;
  17.                 }
  18.                 else if ((pixrow[i - 1] + pixrow[i - 2] + pixrow[i + 1] + pixrow[i + 2]) <= 1) {
  19.                         pixrow[i] = 0;
  20.                 }
  21.         }

之后记录像素为 0 和 1 所连续的长度来计算字符的宽度,最后用宽度的大小来筛选字符。

  1. // 确认字符位置
  2.         int count = 0;
  3.         bool flage = false;
  4.         for (int i = 0; i < roi_col - 1; i++) {
  5.                 pix = pixrow[i];
  6.                 if (pix == 1 && !flage) {
  7.                         flage = true;
  8.                         position1[count] = i;
  9.                         continue;
  10.                 }
  11.                 if (pix == 0 && flage) {
  12.                         flage = false;
  13.                         position2[count] = i;
  14.                         count++;
  15.                 }
  16.                 if (i == (roi_col - 2) && flage) {
  17.                         flage = false;
  18.                         position2[count] = i;
  19.                         count++;
  20.                 }
  21.         }

分割出的结果:


完整代码:
  1. #include <iostream>  
  2. #include <opencv2/highgui/highgui.hpp>  
  3. #include <opencv2/imgproc.hpp>
  4. #include <opencv2/imgproc/types_c.h>
  5. using namespace std;
  6. using namespace cv;
  7. int main() {
  8.         Mat img = imread("number.jpg");
  9.         Mat gray_img;
  10.         // 生成灰度图像
  11.         cvtColor(img, gray_img, CV_BGR2GRAY);
  12.         // 高斯模糊
  13.         Mat img_gau;
  14.         GaussianBlur(gray_img, img_gau, Size(3, 3), 0, 0);
  15.         // 阈值分割
  16.         Mat img_threadhold;
  17.         threshold(img_gau, img_threadhold, 0, 255, THRESH_BINARY + THRESH_OTSU);
  18.         // 判断字符水平位置
  19.         int roi_col = img_threadhold.cols, roi_row = img_threadhold.rows, position1[50], position2[50], roi_width[50];
  20.         uchar pix;
  21.         // 确认为 1 的像素
  22.         int pixrow[1000];
  23.         for (int i = 0; i < roi_col - 1; i++) {
  24.                 for (int j = 0; j < roi_row - 1; j++) {
  25.                         pix = img_threadhold.at<uchar>(j, i);
  26.                         pixrow[i] = 0;
  27.                         if (pix > 0) {
  28.                                 pixrow[i] = 1;
  29.                                 break;
  30.                         }
  31.                 }
  32.         }
  33.         // 对数组进行滤波,减少突变概率
  34.         for (int i = 2; i < roi_col - 1 - 2; i++) {
  35.                 if ((pixrow[i - 1] + pixrow[i - 2] + pixrow[i + 1] + pixrow[i + 2]) >= 3) {
  36.                         pixrow[i] = 1;
  37.                 }
  38.                 else if ((pixrow[i - 1] + pixrow[i - 2] + pixrow[i + 1] + pixrow[i + 2]) <= 1) {
  39.                         pixrow[i] = 0;
  40.                 }
  41.         }
  42.         // 确认字符位置
  43.         int count = 0;
  44.         bool flage = false;
  45.         for (int i = 0; i < roi_col - 1; i++) {
  46.                 pix = pixrow[i];
  47.                 if (pix == 1 && !flage) {
  48.                         flage = true;
  49.                         position1[count] = i;
  50.                         continue;
  51.                 }
  52.                 if (pix == 0 && flage) {
  53.                         flage = false;
  54.                         position2[count] = i;
  55.                         count++;
  56.                 }
  57.                 if (i == (roi_col - 2) && flage) {
  58.                         flage = false;
  59.                         position2[count] = i;
  60.                         count++;
  61.                 }
  62.         }
  63.         // 记录所有字符宽度
  64.         for (int n = 0; n < count; n++) {
  65.                 roi_width[n] = position2[n] - position1[n];
  66.         }
  67.         // 减去最大值、最小值,计算平均值用字符宽度来筛选
  68.         int max = roi_width[0], max_index = 0;
  69.         int min = roi_width[0], min_index = 0;
  70.         for (int n = 1; n < count; n++) {
  71.                 if (max < roi_width[n]) {
  72.                         max = roi_width[n];
  73.                         max_index = n;
  74.                 }
  75.                 if (min > roi_width[n]) {
  76.                         min = roi_width[n];
  77.                         min_index = n;
  78.                 }
  79.         }
  80.         int index = 0;
  81.         int new_roi_width[50];
  82.         for (int i = 0; i < count; i++) {
  83.                 if (i == min_index || i == max_index) {}
  84.                 else {
  85.                         new_roi_width[index] = roi_width[i];
  86.                         index++;
  87.                 }
  88.         }
  89.         // 取后面三个值的平均值
  90.         int avgre = (int)((new_roi_width[count - 3] + new_roi_width[count - 4] + new_roi_width[count - 5]) / 3.0);
  91.         // 字母位置信息确认,用宽度来筛选
  92.         int licenseX[10], licenseW[10], licenseNum = 0;
  93.         int countX = 0;
  94.         for (int i = 0; i < count; i++) {
  95.                 if (roi_width[i] >(avgre - 8) && roi_width[i] < (avgre + 8)) {
  96.                         licenseX[licenseNum] = position1[i];
  97.                         licenseW[licenseNum] = roi_width[i];
  98.                         licenseNum++;
  99.                         countX++;
  100.                         continue;
  101.                 }
  102.                 if (roi_width[i] > (avgre * 2 - 10) && roi_width[i] < (avgre * 2 + 10)) {
  103.                         licenseX[licenseNum] = position1[i];
  104.                         licenseW[licenseNum] = roi_width[i];
  105.                         licenseNum++;
  106.                 }
  107.         }
  108.         // 截取字符
  109.         Mat number_img = Mat(Scalar(0));
  110.         for (int i = 0; i < countX; i++) {
  111.                 Rect choose_rect(licenseX[i], 0, licenseW[i], gray_img.rows);
  112.                 number_img = gray_img(choose_rect);
  113.                 imshow("number" + to_string(i), number_img);
  114.                 // imwrite("number" + to_string(i) + ".jpg", number_img);
  115.         }
  116.         waitKey(0);
  117.         return 0;
  118. }