• 实战:QT车牌识别系统综合设计


           该系统是博主结合许多QT开发项目综合制作,借用了Opencv的开发库来完成的一个项目,具体的可以按照目录来,关于识别方面仅仅提供一个思路,目前还在想如何去优化(准备采用神经网络将数据集拟合,减少时间复杂度,目前还在实现...)。因为识别时间太长会很影响效率,具体的安装我不在这里讲解

            QT版本:5.9.9

            Opencv:  4.5.1

    一、车牌提取

    1、图像预处理

            首先我们将图像读取出来用imread函数输入图片路径

    Mat image = imread("D:\\qthome\\test_car\\test2.png");

             之后我们将图像进行高斯去噪去除一些图像的杂质,之后灰度处理转化成黑白图,进行边缘检测将车牌的轮廓检测出来,使用自适应阀值使图像的灰度大于阀值以便跟好的提取

    1. Mat img_HSV;//原图
    2. GaussianBlur(img_HSV, img2_gauss, Size(3, 3), 0);//高斯去噪
    3. cvtColor(img2_gauss, img2_gray, COLOR_BGR2GRAY);//灰度处理
    4. /*边缘检测*/
    5. Sobel(img2_gray, dst_x, CV_16S, 1, 0); //梯度算子
    6. convertScaleAbs(dst_x, abs_X); //将CV_16S型的输出图像转变成CV_8U型的图像
    7. Mat img_temo = abs_X;
    8. threshold(img_temo, img_temo, 0, 255, THRESH_OTSU); //自适应阀值图像灰度大于阈值
    9. imshow("tests",img_temo);

    效果如下:

    2、轮廓封闭提取

            首先对矩形进行封闭操作,长宽比为17:5,然后进行膨胀和腐蚀,将腐蚀的面积扩大,具体的使用函数如下:

    图像膨胀:
       

    图像腐蚀:

          

    2个函数的意思相近,基本上都是输入原图,然后用一个相同类型的变量去接收,第三个参数进行怎样的处理,具体腐蚀几下和膨胀几下根据效果来

    1. // 闭操作,封闭轮廓
    2. Mat kernelY;
    3. Mat kernelX = getStructuringElement(MORPH_RECT, Size(17, 5)); //构造一个矩形
    4. morphologyEx(img_temo, img_temo, MORPH_CLOSE, kernelX);
    5. //用矩形来封闭
    6. kernelX = getStructuringElement(MORPH_RECT,Size(20, 1));
    7. kernelY = getStructuringElement(MORPH_RECT,Size(1, 19));
    8. dilate(img_temo, img_temo, kernelX); //图像膨胀
    9. dilate(img_temo, img_temo, kernelX);
    10. erode(img_temo, img_temo, kernelX); //图像腐蚀
    11. erode(img_temo, img_temo, kernelY);
    12. dilate(img_temo, img_temo, kernelY);
    13. // 平滑去噪处理,使边缘检测更准确
    14. GaussianBlur(img_temo, img_temo, Size(15, 0), 1);

    效果如下:

     

    随后将轮廓在原图勾画出来

    1. vector> contours;
    2. vector vec_4f;
    3. // 从二值化后的img图中提取所有轮廓
    4. findContours(img_temo, contours, vec_4f, RETR_TREE, CHAIN_APPROX_SIMPLE);
    5. // 在原图image上绘制所有轮廓(红色)
    6. drawContours(image, contours, -1, Scalar(0, 0, 255), 1);
    7. imshow("car_num",image);

     

    3、车牌号提取

            将区域进行比对筛选,选择长宽比例为1:2.3(这个参数具体可以设置,根据自己的感觉来,不一定参考这个)面积选择10000左右的即可

    1. Mat chepai;
    2. int j = 0;
    3. for (int i = 0; i < contours.size(); i++)
    4. {
    5. //计算轮廓的垂直边界最小矩形
    6. Rect rect = boundingRect(contours[i]);
    7. int x = rect.x;
    8. int y = rect.y;
    9. int area = rect.height * rect.width;
    10. if ((rect.width > (rect.height * 2.3) )&& area > 10000)
    11. {
    12. chepai = img_HSV(Rect(rect.x,rect.y,rect.width,rect.height)); //区域提取
    13. //将提取出来的区域拿绿色矩形围起来
    14. rectangle(chepai,Point(rect.x,rect.y),Point(rect.x + rect.width, rect.y + rect.height),Scalar(0,255,0),1);
    15. }
    16. }

            最后车牌也被提取了出来

    效果如下:

     车牌提取的完整代码:

    1. Mat getcarnumber_Border(Mat image)
    2. {
    3. Mat img_HSV = image.clone();//复制
    4. Mat img_t = image.clone();
    5. Mat img2_gauss;
    6. Mat img2_gray,dst_x,abs_X;
    7. //高斯去噪
    8. GaussianBlur(img_HSV, img2_gauss, Size(3, 3), 0);
    9. cvtColor(img2_gauss, img2_gray, COLOR_BGR2GRAY);
    10. Sobel(img2_gray, dst_x, CV_16S, 1, 0); //梯度算子
    11. convertScaleAbs(dst_x, abs_X); //将CV_16S型的输出图像转变成CV_8U型的图像
    12. imshow("tests1",dst_x);
    13. Mat img_temo = abs_X;
    14. threshold(img_temo, img_temo, 0, 255, THRESH_OTSU); //用这个函数,自适应阀值图像灰度大于阈值
    15. imshow("tests",img_temo);
    16. // 闭操作,封闭轮廓
    17. Mat kernelY;
    18. Mat kernelX = getStructuringElement(MORPH_RECT, Size(17, 5)); //构造一个矩形
    19. morphologyEx(img_temo, img_temo, MORPH_CLOSE, kernelX);
    20. //用矩形来封闭
    21. kernelX = getStructuringElement(MORPH_RECT,Size(20, 1));
    22. kernelY = getStructuringElement(MORPH_RECT,Size(1, 19));
    23. dilate(img_temo, img_temo, kernelX); //图像膨胀
    24. dilate(img_temo, img_temo, kernelX);
    25. erode(img_temo, img_temo, kernelX); //图像腐蚀
    26. erode(img_temo, img_temo, kernelY);
    27. dilate(img_temo, img_temo, kernelY);
    28. // 平滑去噪处理,使边缘检测更准确
    29. GaussianBlur(img_temo, img_temo, Size(15, 0), 1);
    30. //imshow("car_num",img_temo);
    31. vector> contours;
    32. vector vec_4f;
    33. // 从二值化后的img图中提取所有轮廓
    34. findContours(img_temo, contours, vec_4f, RETR_TREE, CHAIN_APPROX_SIMPLE);
    35. // 在原图image上绘制所有轮廓(红色)
    36. // drawContours(image, contours, -1, Scalar(0, 0, 255), 1);
    37. // imshow("car_num",image);
    38. //筛选
    39. Mat chepai;
    40. int j = 0;
    41. vector>contours_1;
    42. Rect rect_1;
    43. for (int i = 0; i < contours.size(); i++)
    44. {
    45. //计算轮廓的垂直边界最小矩形
    46. Rect rect = boundingRect(contours[i]);
    47. int x = rect.x;
    48. int y = rect.y;
    49. int area = rect.height * rect.width;
    50. if ((rect.width > (rect.height * 2.3) )&& area > 10000)
    51. {
    52. chepai = img_HSV(Rect(rect.x,rect.y,rect.width,rect.height)); //区域提取
    53. //将提取出来的区域拿绿色矩形围起来
    54. rectangle(chepai,Point(rect.x,rect.y),Point(rect.x + rect.width, rect.y + rect.height),Scalar(0,255,0),1);
    55. rect_1 = rect;
    56. contours_1.push_back(contours[i]);
    57. }
    58. }
    59. // drawContours(img_t, contours_1, -1, Scalar(0, 0, 255), 1);
    60. // imshow("sda",chepai);
    61. //对倾斜车牌进行处理操作
    62. // vector cnt = contours_1[contours_1.size() - 1];
    63. // Vec4f d;
    64. // fitLine(cnt,d,DIST_L2,0,0.01,0.01);
    65. // double k = d[1] / d[0];
    66. // double b = d[2] - d[3] * k;
    67. // double w = img_t.size().width;
    68. // double h = img_t.size().height;
    69. // double lefty = b;
    70. // double righty = k*w+b;
    71. // double a = atan(k);
    72. // a = a*180/3.1415926;
    73. // Mat m = getRotationMatrix2D(Point(w/2,h/2),a,0.8);
    74. // Mat dst;
    75. // warpAffine(img_t,dst,m,Size((int(w*0.9)),(int(h*0.9))));
    76. // dst = do_line(dst);
    77. // if(dst.empty()){
    78. // return chepai;
    79. // }
    80. return chepai;
    81. }

    二、车牌字符串切割

    1、车牌号码处理

            车牌号码提取出来之后我们也要对他进行相同的如以上相同的处理,最后转化为黑白图,但重点不同的是蓝牌、黄牌以及新能源汽车的车牌型号都不同,为此我们对像素点进行将车牌彻底转化为背景为黑色,字为白色的情况

    效果1(蓝色车牌的黑白图):

    效果2(新能源车牌的黑白图):

    所以就统计了黑白像素点的个数,确保黑像素点个数大于白像素点个数,否则进行翻转

    1. Mat Get_License_ROI(Mat src)
    2. {
    3. Mat gray;
    4. cvtColor(src, gray, COLOR_BGR2GRAY);
    5. Mat thresh;
    6. threshold(gray, thresh, 0, 255, THRESH_OTSU);
    7. //使用形态学开操作去除一些小轮廓
    8. Mat kernel = getStructuringElement(MORPH_RECT, Size(2, 2));
    9. Mat open;
    10. morphologyEx(thresh, open, MORPH_OPEN, kernel);
    11. //下面是我自己写的,这个可以自己去实现
    12. int black = PixelCounter(open,2);
    13. int white = PixelCounter(open,1);
    14. qDebug()<<"黑像素点"<"白像素点"<
    15. if(black < white){
    16. threshold(open, open, 0, 255, THRESH_OTSU|THRESH_BINARY_INV);
    17. }
    18. return open;
    19. }

    效果如下:

    2、水平切割去除多余的边框

            我是采取将每一行的白像素点求一个极限阀值,然后从中间一行往两边延伸,当碰到一个阀值时候就停止扩展,因为如上图,白色边框的白像素的个数明显多余中间的白像素点,最后为了方便切割提取将字符膨胀

    代码:

    1. Mat Horizon_Cut(Mat image){
    2. Mat temp = image.clone();
    3. int rows = temp.rows;
    4. int cols = temp.cols;
    5. QVector<int> white_n;
    6. for(int row = 0;row < rows;row++){
    7. int tem = 0;
    8. for(int col = 0;col < cols;col++){
    9. if(temp.at(row, col) > 0){
    10. tem++;
    11. }
    12. }
    13. white_n.append(tem);
    14. }
    15. int mid = 0;
    16. for(int i = 0;i < rows/2;i++){
    17. mid += white_n[i];
    18. }
    19. mid = mid/(rows/2 + 1);
    20. int i_0;
    21. int i_1;
    22. for(int i = rows/2 ;i >= 2; i--){
    23. if(white_n[i] < mid){
    24. i_0 = i;
    25. break;
    26. }
    27. }
    28. for(int i = rows/2;i < rows; i++){
    29. if(white_n[i] < mid){
    30. i_1 = i;
    31. break;
    32. }
    33. }
    34. Mat t_est = temp(Range(i_0,i_1),Range(0,cols));
    35. return t_est;
    36. }

    效果如下:

    3、字符串逐步提取

            我是采取从左往右进行逐步遍历切割,腐蚀膨胀后的字符串白像素点的间隔明显很大,这个可以注释慢慢去比对,一定距离进行切割,保存每个字符的前后2列数据即可

    1. QVectorint> > Remove_Vertial_Border(Mat image)
    2. {
    3. Mat temp = image.clone();
    4. int rows = temp.rows;
    5. int cols = temp.cols;
    6. QVector<int> white_n;
    7. for(int col = 0;col < cols;col++){
    8. int tem = 0;
    9. for(int row = 0;row < rows;row++){
    10. if(temp.at(row, col) > 0){
    11. tem++;
    12. }
    13. }
    14. white_n.append(tem);
    15. }
    16. QVectorint> > region1;
    17. QVector<int> reg;
    18. int flag = 0;
    19. if(white_n[0] != 0){
    20. reg.append(0);
    21. }
    22. for(int i = 0;i1;i++){
    23. if(white_n[i] == 0 && white_n[i + 1] != 0){
    24. reg.append(i);
    25. }
    26. if(white_n[i] != 0 && white_n[i + 1] == 0){
    27. reg.append(i + 1);
    28. }
    29. if(reg.size() == 2){
    30. if(reg[1] - reg[0] > 10){
    31. region1.append(reg);
    32. reg.clear();
    33. }else{
    34. reg.clear();
    35. }
    36. }
    37. }
    38. return region1;
    39. }

    之后将保存的每一个字符元素返回,写入新的文件夹

    1. QVectorint> > t = Remove_Vertial_Border(car_gray);
    2. int j = 0;
    3. for (int i = 0; i < t.size(); i++)
    4. {
    5. Mat str = car_gray(Range(0,car_gray.rows),Range(t[i][0],t[i][1]));
    6. j++;
    7. QString t_s = QString::number(j);
    8. string s = string((const char *)t_s.toLocal8Bit());
    9. imwrite("D:\\qthome\\test_car\\car_each_number\\"+s+".jpg",str);
    10. }

    效果如下: 

    三、图片识别

            车牌识别除了字符串提取的难点另一个难点就是识别,识别的话,我是自己准备了一个训练集,每个训练集中包含了多种数据

            将每个图文文件中图片的路径保存在列表当中

    之后使用MatchTemplate函数讲每个图片进行比对(也许这里很大程度增加了时间复杂度,但是我目前是保证能做出来优先,时间复杂度之后慢慢优化)

     而method方法有很多

    cv::TM_SQDIFF:该方法使用平方差进行匹配,因此最佳的匹配结果在结果为0处,值越大匹配结果越差。

    cv::TM_SQDIFF_NORMED:该方法使用归一化的平方差进行匹配,最佳匹配也在结果为0处。

    cv::TM_CCORR:相关性匹配方法,该方法使用源图像与模板图像的卷积结果进行匹配,因此,最佳匹配位置在值最大处,值越小匹配结果越差。

    cv::TM_CCORR_NORMED:归一化的相关性匹配方法,与相关性匹配方法类似,最佳匹配位置也是在值最大处。

    cv::TM_CCOEFF:相关性系数匹配方法,该方法使用源图像与其均值的差、模板与其均值的差二者之间的相关性进行匹配,最佳匹配结果在值等于1处,最差匹配结果在值等于-1处,值等于0直接表示二者不相关。

    cv::TM_CCOEFF_NORMED:归一化的相关性系数匹配方法,正值表示匹配的结果较好,负值则表示匹配的效果较差,也是值越大,匹配效果也好。

     这里可以选择自己喜欢的,我是选择了第5个

    1. double do_read(Mat temo,Mat temp){
    2. int height = temo.size().height;
    3. int width = temo.size().width;
    4. Mat image = temp.clone();
    5. resize(image,image,Size(width,height));
    6. Mat result;
    7. matchTemplate(image,temo,result,TM_CCOEFF);
    8. //求解最大值
    9. double minValue, maxValue;
    10. Point minLocation, maxLocation;
    11. Point matchLocation;
    12. minMaxLoc(result, &minValue, &maxValue, &minLocation, &maxLocation);
    13. return maxValue;
    14. }

            车牌里面没有I和O目的就是为了区分0和1而汉字只在第一个出现,后面的则是一些字符所以我进行了分开识别,最后找到该图片中最大相似度的坐标(最大坐标函数是我自己写的,这里我就不详写了)进行锁定

    1. void shibie(){
    2. Mat temp = imread("D:\\qthome\\car_stop_t\\car_each_number\\1.jpg");
    3. cvtColor(temp, temp, COLOR_BGR2GRAY);
    4. threshold(temp, temp, 0, 255, THRESH_OTSU);
    5. QString str;
    6. QVector<double> score;
    7. for(int i = 0;i < 31;i++){
    8. double Max = 0;
    9. for(int j = 0;j < chinese_str[i].size();j++){
    10. string str = string((const char *)chinese_str[i][j].toLocal8Bit());
    11. Mat temo = imread(str);
    12. cvtColor(temo,temo,COLOR_RGB2GRAY);
    13. threshold(temo,temo,0,255,THRESH_OTSU);
    14. double maxValue = do_read(temo,temp);
    15. if(maxValue > Max) Max = maxValue;
    16. }
    17. score.append(Max);
    18. }
    19. int posmax = Max_index(score);
    20. str.append(chineses[posmax]);
    21. for(int i = 2;i <= 8;i++){
    22. QString str_1 = "D:\\qthome\\car_stop_t\\car_each_number\\"+QString::number(i)+".jpg";
    23. temp = imread(string((const char *)str_1.toLocal8Bit()));
    24. cvtColor(temp, temp, COLOR_BGR2GRAY);
    25. threshold(temp, temp, 0, 255, THRESH_OTSU);
    26. score.clear();
    27. for(int i = 0;i < 34;i++){
    28. double Max = 0;
    29. for(int j = 0;j < char_str[i].size();j++){
    30. string str = string((const char *)char_str[i][j].toLocal8Bit());
    31. Mat temo = imread(str);
    32. cvtColor(temo,temo,COLOR_RGB2GRAY);
    33. threshold(temo,temo,0,255,THRESH_OTSU);
    34. double maxValue = do_read(temo,temp);
    35. if(maxValue > Max) Max = maxValue;
    36. }
    37. score.append(Max);
    38. }
    39. posmax = Max_index(score);
    40. str.append(chars[posmax]);
    41. }
    42. qDebug()<
    43. }

    效果展示:

    四、总结:

    1、项目中整体难点在于车牌处理以及像素点的切割,合理的处理这些对后续的切割有很大帮助

    2、字符串的识别速率还有待提高,但是精度方面的话已经完善

    最后给观看的人看一个系统雏形旁边的mv纯纯是为了区别识别慢,这个还需要突破,希望还是能带来帮助 

  • 相关阅读:
    MATLAB/Python编程 | 图片的形态学处理
    业绩不俗,毛利率下滑,股价接连下跌,片仔癀将向何处去?
    金仓数据库 KingbaseES 插件参考手册(22. dbms_sql)
    可口可乐用新的“Y3000”口味拥抱有争议的人工智能图像生成器
    【html-CSS布局】简单设计一个静态网页
    基于Java+SpringBoot+Vue前后端分离电商应用系统设计和实现
    彩虹女神跃长空,Go语言进阶之Go语言高性能Web框架Iris项目实战-模板与数据库EP02
    实现VLAN间通信&以太网链路聚合与交换机堆叠、集群&华为ICT网络赛道
    全球10大智慧港口介绍
    Windows 输入法在注册表中的管理
  • 原文地址:https://blog.csdn.net/qq_54395977/article/details/125921456