最近在复习OPENCV的知识,学习caffe的深度神经网络,正好想起以前做过的车牌识别项目,可以拿出来研究下
以前的环境是VS2013和OpenCV2.4.9,感觉OpenCV2.4.9是个经典版本啊!不过要使用caffe模型的话,还是要最新的OpenCV3.3更合适!
一、车牌图片库
以前也是网上下的,如果找不到的小伙伴可以从我这儿下: 链接:http://pan.baidu.com/s/1hrQF92G 密码:43jl
里面有数字 “0-9”,字母“A-Z”的训练图片各50张。
测试车牌图片当时是从他人得到已经定位到车牌的图片,类似如下:
目标当然就是对这些车牌图片进行预处理,单字符分割,单字符识别!
二、预处理
图像的预处理做来做去就是滤波去噪,光照补偿,灰度/二值化,形态学基本操作等等。这些图片都是自然场景得到所以基本的去噪操作可以做一下,然后为了单字符分割,灰度化和形态学可以结合效果调整。
光照补偿其实一直是个问题,大多数有直方图均衡化,亮度参考白,利用公式统计补偿图片。这方面也可以结合图像增强方法来做!笔者当时觉得前两者对大多数场景已经适用。
二值化可以使用 cv::threshold函数,如:
1 Mat t1=imread("2.png",1); 2 cvtColor(inimg, gimg, CV_BGR2GRAY); 3 threshold(gimg, gimg, 100, 255, CV_THRESH_BINARY); 4 imshow("gimg", gimg);
第一行imread(),由于flag设为1所以读的是彩图,采用cvtColor函数转化为灰度图。如果你读入就是灰度图可以省略第二行代码。第三行就是转化为二值化函数,阈值100可以修改,在灰度对比不明显是有必要!
效果:
如果预处理做的好,某些小的白色区域是可以去掉的。这个效果也可以识别。
同时可以发现车牌外围被一圈白色包围,如若能去除外围白色,对于单字符分割更有益。但其实通过寻找列像素之间的变化,白色区域只是影响了阈值不会对结果太大影响。
想要去除白色外圈可以参考:http://blog.csdn.net/u011630458/article/details/43733057
如果想要使用直方图均衡化,OPENCV有equalizeHist(inputmat, outputmat);非常方便,但是效果不好。
使用直方图均衡化后的上述车牌二值化图片:
效果更惨烈了,因为均衡化就是让直方图的像素分布更加平衡,上图黑色多,均衡之后自然白色多了,反而不好!
二、单字符分割
单字符分割主要策略就是检测列像素的总和变化,因为没有字符的区域基本是黑色,像素值低;有字符的区域白色较多,列像素和就变大了!
列像素变化的阈值是个问题,看到很多博客是固定的阈值进行检测,除非你处理后的二值化图像非常完美,不然有的图片混入了白色区域就会分割错误!而且对于得到分割宽度如果太小也应该使用策略进行剔除,没有一定的宽度限制分割后的图片可能是很多个窄窄的小区域。。。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 int getColSum(Mat& bimg, int col) 2 { 3 int height = bimg.rows; 4 int sum = 0; 5 for (int i = 1; i < height; i++) 6 { 7 sum += bimg.at<uchar>(i, col); 8 } 9 cout << sum << endl; 10 return sum; 11 } 12 13 int cutLeft(Mat& src, int Tsum, int right)//左右切割 14 { 15 int left; 16 left = 0; 17 18 int i; 19 for (i=0; i < src.cols; i++) 20 { 21 int colValue = getColSum(src, i); 22 if (colValue> Tsum) 23 { 24 left = i; 25 break; 26 } 27 } 28 int roiWidth=src.cols/7; 29 for (; i < src.cols; i++) 30 { 31 int colValue = getColSum(src, i); 32 if (colValue < Tsum) 33 { 34 right = i; 35 if ((right - left) < (src.cols/7)) 36 continue; 37 else 38 { 39 roiWidth = right - left; 40 break; 41 } 42 43 } 44 } 45 return roiWidth; 46 } 47 48 int getOne(Mat& inimg) 49 { 50 Mat gimg,histimg; 51 cvtColor(inimg, gimg, CV_BGR2GRAY); 52 equalizeHist(gimg,histimg); 53 //imshow("histimg", histimg); 54 threshold(gimg, gimg, 100, 255, CV_THRESH_BINARY); 55 imshow("gimg", gimg); 56 waitKey(0); 57 58 int psum=0; 59 for (int i = 0; i < gimg.cols; i++) 60 { 61 psum+=getColSum(gimg, i); 62 } 63 cout <<"psum/col:"<< psum/gimg.cols << endl; 64 int Tsum = 0.6*(psum / gimg.cols); 65 int roiWid= cutLeft(gimg, Tsum, 0); 66 67 return roiWid; 68 }
笔者思路也很简单:
首先统计所有列像素的总和,取其列像素的均值作为参考标准之一(也可以选用其他数学指标参考),列像素的阈值Tsum设置为列像素均值的百分比(如60%,是情景定)。
利用cutLeft()函数对图片进行列扫描,将列像素超过阈值的列标记为左边,再继续寻找右边,将满足阈值的右边进行标记。左右相减即可得到宽度分割字符。
考虑到车牌中只有7个字符,所以先判断得到宽度大小,如果小于总宽的七分之一视为干扰放弃;其实也可以加大到总宽的8分之一(因为车牌中间可能有连接符)。
getColSum()函数是求一列的像素和,这里用到了.at<> 方式,其实还有别的方法也可以,只要获得当前的像素值,并累加整列即可!
上图车牌的分割效果:
因为第三张有车牌的连接符,所以导致第三张和第四张稍有瑕疵,但总体分割还是满意的!
三、单字符识别
只论字符识别其实有不少选择方案,一开始笔者尝试了ORB特征,想利用特征匹配计算相似度来判断最优的字符结果。ORB特征相比SURF/SIFT更加快速,而且特征不变性也不错。但是在匹配时发现单字符的图片像素点太少,提取的特征点数极少,无法得到较好的匹配结果,只能放弃!
其实也有模板匹配来做字符识别的,但是OPENCV提供的模板匹配对于从同一副图片提取的模板图去匹配样本图效果很好,不是同一副图片时效果很一般。因为笔者用OPENCV的模板匹配一般用来找重复区域。
OCR识别是可以完全用在此处的,OCR识别甚至可以识别汉字,安装OCR的库之后就可以尝试一番!
笔者最后选择了神经网络ANN来做字符分类识别,利用SVM也可以都是分类器之一。使用神经网络可以和caffe的mnist模型有所对比的感觉!
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 void ann10(Mat& testroi) 2 { 3 const string fileform = "*.png"; 4 const string perfileReadPath = "E:\\vswork\\charSamples"; 5 6 const int sample_mun_perclass = 50;//训练字符每类数量 7 const int class_mun = 34;//训练字符类数 0-9 A-Z 除了I、O 8 9 const int image_cols = 8; 10 const int image_rows = 16; 11 string fileReadName,fileReadPath; 12 char temp[256]; 13 14 float trainingData[class_mun*sample_mun_perclass][image_rows*image_cols] = { { 0 } };//每一行一个训练样本 15 float labels[class_mun*sample_mun_perclass][class_mun] = { { 0 } };//训练样本标签 16 17 for (int i = 0; i <= class_mun - 1; i++)//不同类 18 { 19 //读取每个类文件夹下所有图像 20 int j = 0;//每一类读取图像个数计数 21 22 if (i <= 9)//0-9 23 { 24 sprintf(temp, "%d", i); 25 //printf("%d\n", i); 26 } 27 else//A-Z 28 { 29 sprintf(temp, "%c", i + 55); 30 //printf("%c\n", i+55); 31 } 32 33 fileReadPath = perfileReadPath + "/" + temp + "/" + fileform; 34 cout << "文件夹" << fileReadPath << endl; 35 36 HANDLE hFile; 37 LPCTSTR lpFileName = StringToWchar(fileReadPath);//指定搜索目录和文件类型,如搜索d盘的音频文件可以是"D:\\*.mp3" 38 WIN32_FIND_DATA pNextInfo; //搜索得到的文件信息将储存在pNextInfo中; 39 hFile = FindFirstFile(lpFileName, &pNextInfo);//请注意是 &pNextInfo , 不是 pNextInfo; 40 if (hFile == INVALID_HANDLE_VALUE) 41 { 42 continue;//搜索失败 43 } 44 //do-while循环读取 45 do 46 { 47 if (pNextInfo.cFileName[0] == '.')//过滤.和.. 48 continue; 49 j++;//读取一张图 50 //wcout<<pNextInfo.cFileName<<endl; 51 //printf("%s\n",WcharToChar(pNextInfo.cFileName)); 52 //对读入的图片进行处理 53 Mat srcImage = imread(perfileReadPath + "/" + temp + "/" + WcharToChar(pNextInfo.cFileName), CV_LOAD_IMAGE_GRAYSCALE); 54 Mat resizeImage; 55 Mat trainImage; 56 Mat result; 57 58 resize(srcImage, resizeImage, Size(image_cols, image_rows), (0, 0), (0, 0), CV_INTER_AREA);//使用象素关系重采样。当图像缩小时候,该方法可以避免波纹出现 59 threshold(resizeImage, trainImage, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); 60 61 for (int k = 0; k<image_rows*image_cols; ++k) 62 { 63 trainingData[i*sample_mun_perclass + (j - 1)][k] = (float)trainImage.data[k]; 64 //trainingData[i*sample_mun_perclass+(j-1)][k] = (float)trainImage.at<unsigned char>((int)k/8,(int)k%8);//(float)train_image.data[k]; 65 //cout<<trainingData[i*sample_mun_perclass+(j-1)][k] <<" "<< (float)trainImage.at<unsigned char>(k/8,k%8)<<endl; 66 } 67 68 } while (FindNextFile(hFile, &pNextInfo) && j<sample_mun_perclass);//如果设置读入的图片数量,则以设置的为准,如果图片不够,则读取文件夹下所有图片 69 70 } 71 72 // Set up training data Mat 73 Mat trainingDataMat(class_mun*sample_mun_perclass, image_rows*image_cols, CV_32FC1, trainingData); 74 cout << "trainingDataMat——OK!" << endl; 75 76 // Set up label data 77 for (int i = 0; i <= class_mun - 1; ++i) 78 { 79 for (int j = 0; j <= sample_mun_perclass - 1; ++j) 80 { 81 for (int k = 0; k < class_mun; ++k) 82 { 83 if (k == i) 84 if (k == 18) 85 { 86 labels[i*sample_mun_perclass + j][1] = 1; 87 } 88 else if (k == 24) 89 { 90 labels[i*sample_mun_perclass + j][0] = 1; 91 } 92 else 93 { 94 labels[i*sample_mun_perclass + j][k] = 1; 95 } 96 else 97 labels[i*sample_mun_perclass + j][k] = 0; 98 } 99 } 100 } 101 Mat labelsMat(class_mun*sample_mun_perclass, class_mun, CV_32FC1, labels); 102 cout << "labelsMat:" << endl; 103 ofstream outfile("out.txt"); 104 outfile << labelsMat; 105 //cout<<labelsMat<<endl; 106 cout << "labelsMat——OK!" << endl; 107 108 //训练代码 109 110 cout << "training start...." << endl; 111 CvANN_MLP bp; 112 // Set up BPNetwork's parameters 113 CvANN_MLP_TrainParams params; 114 params.train_method = CvANN_MLP_TrainParams::BACKPROP; 115 params.bp_dw_scale = 0.001; 116 params.bp_moment_scale = 0.1; 117 params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, 10000, 0.0001); //设置结束条件 118 //params.train_method=CvANN_MLP_TrainParams::RPROP; 119 //params.rp_dw0 = 0.1; 120 //params.rp_dw_plus = 1.2; 121 //params.rp_dw_minus = 0.5; 122 //params.rp_dw_min = FLT_EPSILON; 123 //params.rp_dw_max = 50.; 124 125 //Setup the BPNetwork 126 Mat layerSizes = (Mat_<int>(1, 5) << image_rows*image_cols, 128, 128, 128, class_mun); 127 bp.create(layerSizes, CvANN_MLP::SIGMOID_SYM, 1.0, 1.0);//CvANN_MLP::SIGMOID_SYM 128 //CvANN_MLP::GAUSSIAN 129 //CvANN_MLP::IDENTITY 130 cout << "training...." << endl; 131 bp.train(trainingDataMat, labelsMat, Mat(), Mat(), params); 132 133 bp.save("../bpcharModel.xml"); //save classifier 134 cout << "training finish...bpModel.xml saved " << endl; 135 return; 136 }
ann10函数主要完成读取图片训练ANN网络的功能。
注意点:
修改图片文件类型 fileform;
修改训练图片路径 perfileReadPath等;
修改训练图片数量 sample_mun_perclass;
修改训练类别数 class_mun;(34类是因为IO与10很像,所以少了两类);
image_cols和image_rows根据自己图片情况修改;
观察代码发现训练文件在工程目录的 bpcharModel.xml;之后调用该网络模型即可,网上有很多网络调用和网络训练没有分开,这样你每预测分类一个字符都要重新训练网络会相当浪费时间的,笔者的渣电脑训练一次就要几分钟,每次分类都训练时间有点伤不起。。。真正的实际应用也是用训练好的网络参数直接调用,速度很快。就像caffe中的深度神经网络,使用网络分类时也只是调用生成好的caffemodel和标签、solver文件就行了,如果还要重新训练一小时根本没有实用性。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 void predictann(Mat testroi) 2 { 3 //测试神经网络 4 CvANN_MLP bp; 5 bp.load("E:\\vswork\\CarNumRecog\\bpcharModel.xml"); 6 const int image_cols = 8; 7 const int image_rows = 16; 8 9 cout << "测试:" << endl; 10 //Mat test_image = imread("E:\\vswork\\charSamples\\3.png", CV_LOAD_IMAGE_GRAYSCALE); 11 Mat test_temp; 12 resize(testroi, test_temp, Size(image_cols, image_rows), (0, 0), (0, 0), CV_INTER_AREA);//使用象素关系重采样。当图像缩小时候,该方法可以避免波纹出现 13 threshold(test_temp, test_temp, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); 14 Mat_<float>sampleMat(1, image_rows*image_cols); 15 for (int i = 0; i<image_rows*image_cols; ++i) 16 { 17 sampleMat.at<float>(0, i) = (float)test_temp.at<uchar>(i / 8, i % 8); 18 } 19 20 Mat responseMat; 21 bp.predict(sampleMat, responseMat); 22 Point maxLoc; 23 double maxVal = 0; 24 minMaxLoc(responseMat, NULL, &maxVal, NULL, &maxLoc); 25 char temp[256]; 26 27 if (maxLoc.x <= 9)//0-9 28 { 29 sprintf(temp, "%d", maxLoc.x); 30 //printf("%d\n", i); 31 } 32 else//A-Z 33 { 34 sprintf(temp, "%c", maxLoc.x + 55); 35 //printf("%c\n", i+55); 36 } 37 38 cout << "识别结果:" << temp << " 相似度:" << maxVal * 100 << "%" << endl; 39 imshow("test_image", testroi); 40 waitKey(0); 41 42 return; 43 }
predictann函数就是调用ann10函数生成的网络模型文件,进行预测分类的功能。
上述车牌的单字符识别效果如下:
可以看到有的相似度很高,有的却很低,也有一些识别错误的,我不再显示。。。
相比之前使用的caffe mnist识别率真的是差距有点大,以后有机会将mnist的模型来识别车牌字符试试~~
度盘失效了,附上我的github地址,里面会传数据集和完整代码!欢迎大家star 和 fork 我~~
https://github.com/chenzhefan/CarNumRecognize.git