车牌识别系统的主要组成部分包括: 车牌图像采集、 车牌图像预处理、 车牌定位、 车牌字符分割和车牌字符识别。
通过摄像头或摄像机拍摄含有车辆牌照的图像,采用拍摄的含有汽车车牌的汽车图片作为原始识别图像。
对采集的车牌图像输入计算机进行预处理, 突出车牌的主要特征, 便于提取车牌信息。图像预处理主要是对待处理图像进行图像格式的转换和压缩、 图像去噪、 图像增强等操作,尽量剔除那些无用的信息。
对车牌图像中的车牌字符区域进行定位, 该过程主要利用车牌区域的特征信息。
基于边缘检测和形态学变换的车牌定位算法
基于颜色特征的车牌定位算法
该算法利用了车牌区域车牌背景和字符具有相对固定的颜色搭配的特性。
常用的车牌定位算法还有: 基于数学形态学车牌定位、 基于投影法的车牌定位、 基于字符纹理分析车牌定位、基于机器学习的车牌定位算法、基于小波变换的车牌定位算法、基于Hough变换和轮廓线法的车牌定位算法等。
为得到车牌信息中的每个字符, 需要对定位后的车牌区域中的字符进行分割。通常在分割之前需对车牌进行倾斜校正。目前常用的倾斜校正算法有Hough变换,Radon变换,PCA方法,旋转投影的方法。
我国的车牌有蓝底白字、黄底黑字、黑底白字、白底黑字等颜色搭配,车牌区域一般由7个字符组成。为了识别出确切的车牌信息,目前最好的解决方案是将车牌区域字符单独提取出来分开识别。目前常用的车牌字符分割方法有投影法、模板匹配法、聚类连通域分析法。
投影法。投影法是目前字符分割最常用的方法,该方法简单直观,利用了字符之间存在的固有空隙。如果将车牌区域进行垂直方向投影,就会存在明显的波峰波谷,通过设定一定阈值区分出字符和空隙,就可以将字符分割出来。
聚类连通域分析法。聚类连通域分析法的基本思想是将相互连通的区域看成一个整体,车牌区域通常由7个字符组成,汉字有可能存在多个连通区域,但是字母和数字正常情况下都是连通的。这样可以通过在二值图像上搜寻连通区域,并得到连通区域的外接矩形,正常情况下汉字区域外可以得到6个连通区域即可分割出6个字符,汉字区域可以结合车牌区域的先验知识确定出,从而完成车牌区域的字符分割。该方法具有很髙的鲁棒性,但是算法设计较复杂,需要综合考虑各种可能出现的情况。
对得到的单个车牌字符进行识别。分类的方法很多一般在车牌识别中通常采用BP神经网络。
在《深入理解OpenCV》这本书中介绍过《基于SVM和神经网络的车牌识别》。尝试书中的方法,如果车牌区域附近垂直边缘较丰富, 车牌区域在图像中的尺度不固定以及背景复杂的话,发现该方法效果比较差。考虑到小区以及停车场等场地,该算法还是有一定的实际效用的。先回顾下该书车牌检测的方法。
Mat grayImage;
// 将汽车车牌的汽车图片 srcImage 转换为灰度图 grayImage
cvtColor(srcImage, grayImage, CV_RGB2GRAY);
Mat blurImage = Mat::zeros(grayImage.size(), grayImage.type());;
blur(grayImage, blurImage, Size(5, 5));
// 提取垂直边缘信息
Mat sobelImage = Mat::zeros(grayImage.size(), grayImage.type());;;
Sobel(blurImage, sobelImage, CV_8U, 1, 0, 3, 1, 0);
// 采用阈值滤波器对图像进行二值化,所采用的阈值由otsu算法得到。
Mat threshImage = Mat::zeros(grayImage.size(), grayImage.type());;;
threshold(sobelImage, threshImage, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
// 闭形态算子,删除再每个竖直边缘性之间的空白区域,并连接有大量边的所有区域
Mat element = getStructuringElement(MORPH_RECT, Size(23, 3));
morphologyEx(threshImage, threshImage, CV_MOP_CLOSE, element);
// 查找轮廓
vector< vector > contours;
vector hierarchy;
findContours(threshImage, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
vector rects;
vector< vector >::iterator itc = contours.begin();
while (itc != contours.end())
{
RotatedRect mr = minAreaRect(Mat(*itc));
// 验证候选区域
if (verifySizes(mr))
{
++itc;
rects.push_back(mr);
}
else
{
itc = contours.erase(itc);
}
}
Mat result;
src.copyTo(result);
Mat input;
src.copyTo(input);
// 漫水填充
for (unsigned int i = 0; i < rects.size(); i++)
{
circle(result, rects[i].center, 30, Scalar(0, 255, 255), -1);
float minSize = (rects[i].size.width < rects[i].size.height) ? rects[i].size.width : rects[i].size.height;
minSize = minSize - minSize * 0.5f;
// Initialize rand and get 5 points around center for floodfill algorithm
srand(time_t(NULL));
// Initialize floodfill parameters and variables
Mat mask;
mask.create(input.rows + 2, input.cols + 2, CV_8UC1);
mask = Scalar::all(0);
int loDiff = 30;
int upDiff = 30;
int connectivity = 4;
int newMaskVal = 255;
int NumSeeds = 10;
Rect ccomp;
int flags = connectivity | (newMaskVal << 8) | CV_FLOODFILL_FIXED_RANGE | CV_FLOODFILL_MASK_ONLY;
for (int j = 0; j < NumSeeds; j++)
{
Point seed;
seed.x = (int)(rects[i].center.x + rand() % (int)minSize - (minSize / 2));
seed.y = (int)(rects[i].center.y + rand() % (int)minSize - (minSize / 2));
circle(result, seed, 1, Scalar(0, 255, 255), -1);
int area = floodFill(input, mask, seed, Scalar(255, 0, 0), &ccomp,
Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags);
}
vector pointsInterest;
Mat_::iterator itMask = mask.begin();
Mat_::iterator end = mask.end();
for (; itMask != end; ++itMask)
{
if (*itMask == 255)
{
pointsInterest.push_back(itMask.pos());
}
}
RotatedRect minRect = minAreaRect(pointsInterest);
if (verifySizes(minRect))
{
// rotated rectangle drawing
Point2f rect_points[4];
minRect.points(rect_points);
for (int j = 0; j < 4; j++)
{
line(result, rect_points[j], rect_points[(j + 1) % 4], Scalar(0, 0, 255), 1, 8);
}
// Get rotation matrix
float r = (float)minRect.size.width / (float)minRect.size.height;
float angle = minRect.angle;
if (r < 1)
{
angle = 90 + angle;
}
Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
// Create and rotate image
Mat img_rotated;
warpAffine(input, img_rotated, rotmat, input.size(), CV_INTER_CUBIC);
// Crop image
Size rect_size = minRect.size;
if (r < 1)
{
swap(rect_size.width, rect_size.height);
}
Mat img_crop;
getRectSubPix(img_rotated, rect_size, minRect.center, img_crop);
Mat resultResized;
resultResized.create(33, 144, CV_8UC3);
resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC);
// Equalize croped image
Mat grayResult;
cvtColor(resultResized, grayResult, CV_BGR2GRAY);
blur(grayResult, grayResult, Size(3, 3));
grayResult = histeq(grayResult);
dst.push_back(Plate(grayResult, minRect.boundingRect()));
}
}
bool verifySizes(cv::RotatedRect candidate)
{
float error = 0.4f;
const float aspect = 4.7272f; // 440.0f / 130.0f;
int min = (int)(15 * aspect * 15);
int max = (int)(125 * aspect * 125);
float rmin = aspect - aspect * error;
float rmax = aspect + aspect * error;
float r = (float)(candidate.size.width / candidate.size.height);
if (r < 1)
{
r = (float)candidate.size.height / candidate.size.width;
}
int area = (int)(candidate.size.height * candidate.size.width);
if ((area < min || area > max) || (r < rmin || r > rmax))
{
return false;
}
else
{
return true;
}
}
一般就是采用特征检测+分类的方法。以HOG+SVM为例。
// 初始化训练数据
int iNumTrain = 800;
Mat trainDataHog;
Mat trainLabel = Mat::zeros(iNumTrain, 1, CV_32S);
// 提取HOG特征,放入训练数据矩阵中
Mat imageSrc;
Mat sizeImage = Mat::zeros(64, 64, CV_8UC1);
for (int i = 0; i < iNumTrain; i++)
{
vector<float> descriptor;
// 图片预处理
imageSrc = imread(vecTrainPath[i].c_str(), 1);
resize(imageSrc, sizeImage, Size(64, 64));
HOGDescriptor *hog = new HOGDescriptor(cvSize(64, 64), cvSize(16, 16),
cvSize(8, 8), cvSize(8, 8), 9);
hog->compute(sizeImage, descriptor, Size(1, 1), Size(0, 0));
if (i == 0)
{
trainDataHog = Mat::zeros(iNumTrain, (int)descriptor.size(), CV_32FC1);
}
int n = 0;
for (vector<float>::iterator iter = descriptor.begin(); iter != descriptor.end(); iter++)
{
trainDataHog.at<float>(i, n) = *iter;
n++;
}
trainLabel.at<int>(i, 0) = vecTrainLabel[i];
}
Ptr svm = SVM::create();
svm->setType(SVM::C_SVC);
svm->setKernel(SVM::RBF);
svm->setDegree(10.0);
svm->setGamma(0.09);
svm->setCoef0(1.0);
svm->setC(10.0);
svm->setNu(0.5);
svm->setP(1.0);
//svm->setClassWeights();
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, (int)1e7, 1e-6));
svm->train(trainDataHog, ROW_SAMPLE, trainLabel);
svm->save("svm_hog_model.xml");
OpenALPR的官方网址:http://www.openalpr.com/
OpenALPR的github地址:https://github.com/openalpr/openalpr
easyPR作者的博客:http://www.cnblogs.com/subconscious/
easyPR的github地址:https://github.com/liuruoze/EasyPR
Mastering OpenCV with Practical Computer Vision Projects这本书中以西班牙的车牌为例,对该源码提供的几张车牌确实效果不错,然而在其他数据集上,只能用不理想来形容,适应的场景有限。但它的处理逻辑清晰明了,把车牌识别划分为了两个过程:即车牌检测和字符识别两个过程。再针对这两个过程分别进行处理,极具参考价值。因为没有实际项目的需求,仅仅也只是自己练练手,所以对两个开源的ALPR软件(国外的OpenALRP以及国内的EasyPR)没有过多的研究。如果有机会再补全该片文章(车牌作为车辆的唯一凭证,应用极具广泛,虽然车牌识别已发展多年,但依旧值得开发,丰富第四屏的应用)。
参考资料:
毛江平,车载云台摄像机的车牌识别系统研究
黄山,车牌识别技术的研究和实现
杨思源,基于OPENCV的车辆牌照识别系统研究
Sina Moayed Baharlou, Fast and Adaptive License Plate Recognition
。。。