随着智能停车场系统的兴起,车牌识别技术又着实火了一把。我也来了兴趣,大致浏览了下当前的现状,发现目前流行的车牌识别技术是基于神经网络的(这方面可参见Mastering OpenCV with Practical Computer Vision Projects一书,由机械工业出版社发行)。人工神经网络这么高深的东西,笔者不太懂,但是作为图像匹配技术出身,我认为这种小case需要那么复杂的东西来完成么?并且训练人工神经网络也是一件麻烦的事情。。。总之在各种不服气之下,笔者基于图像匹配技术编写了一个车牌自动识别算法,自己测试了几十张图像,自我感觉效果不错,所以拿出来显摆显摆,希望大家批评指正。
先交代一下开发环境吧,本人基于OpenCV2.4.10,用VS2010开发,配合使用了WinGSL数学库,这些东西网上都有的下,我就不多说了。接下来言归正传。
我设计的车牌自动识别算法可大致分为7个大步骤,其中3个主要步骤中都使用了图像匹配技术,具体步骤如下:
1,基于颜色的车牌分割得到二值化的图像,这一步主要基于绝大多数的车牌都是蓝色背景这一特点(非标非蓝底车牌的识别,需要相对复杂,本文暂不讨论)。
//////////////////////////////////////////////////////////////////////////
// Step1: segment blue background
//////////////////////////////////////////////////////////////////////////
void SegmentBackground2(Mat& img, Mat& binary)
{
binary.create(2, img.size, CV_8UC1);
unsigned char *pSrcData, * pBinData;
int width = img.cols;
int height= img.rows;
unsigned char* data = binary.ptr();
memset(data, 0xFF, binary.step * height);
int i, j;
double total = 0.0f;
int counter = 0;
unsigned char blue, green, red;
for (i = 0; i < height; ++ i)
{
pSrcData = img.ptr(i);
pBinData = binary.ptr(i);
int k = 0;
for (j = 0; j < width; ++ j)
{
blue = pSrcData[k];
green= pSrcData[k + 1];
red = pSrcData[k + 2];
double ave = (blue + green + red) / 3.0f;
if (blue * 0.75 > green && green * 0.80 > red && ave > 32)
{
pBinData[j] = 0;
++ counter;
total += blue;
}
k += 3;
}
}
}
2. 对分割的结果进行同时进行连通区域标记和轮廓跟踪,根据轮廓对于每一个标记区域计算最小包围盒;根据连通区域面积对各独立区域进行排序。然后从大到小,按一定的准则开始合并这些连通区域,最终确定车牌区域。
int LabelBackgroundImg(Mat& binary, Mat& labelImg, std::vector<int>& area, std::vector<CContour>& exContours)
{
IplImage iplBinary = binary;
labelImg.create(2, binary.size, CV_32S);
int* pLabel = (int*)(labelImg.data);
int num = LabelImagewithExContours(&iplBinary, pLabel, exContours);
area.assign(num, 0);
int w = binary.cols;
int h = binary.rows;
for (int i = 0; i < h; ++ i)
{
int* labelDat = (int*)(labelImg.ptr(i));
for (int j = 0; j < w; ++ j)
{
if (labelDat[j] > 0)
{
int n = labelDat[j] - 1;
++ area[n];
}
}
}
return num;
}
void UniteComponets(std::vector<int>& area, std::vector<CContour>& exContours, RotatedRect& box, std::vector<int>& labels)
{
int n2 = exContours.size();
int nn = area.size();
int* ids = new int[nn];
int* area_ = new int[nn];
for (int i = 0; i < nn; ++ i){
ids[i] = i;
area_[i] = area[i];
}
for (int i = 0; i < nn - 1; ++ i)
{
for (int j = i + 1; j < nn; ++ j)
{
if (area_[i] < area_[j])
{
int t = area_[i];
area_[i] = area_[j];
area_[j] = t;
t = ids[i];
ids[i] = ids[j];
ids[j] = t;
}
}
}
delete[] area_;
int id = ids[0];
CPointSet ptSet;
ptSet.Resize(exContours[id].GetLenth());
exContours[id].ExportPointSet(ptSet);
int orgPtNum = ptSet.GetPntsCount();
CvPoint* orgPts = new CvPoint[orgPtNum];
ptSet.ExportData(orgPts);
Mat orgPtMat(1, orgPtNum, CV_32SC2, orgPts);
RotatedRect orgBox = minAreaRect(orgPtMat);
labels.clear();
labels.push_back(id);
for (int i = 1; i < nn; ++ i)
{
int id = ids[i];
ptSet.Resize(exContours[id].GetLenth());
exContours[id].ExportPointSet(ptSet);
int ptNum = ptSet.GetPntsCount();
CvPoint* pts = new CvPoint[orgPtNum + ptNum];
ptSet.ExportData(pts + orgPtNum);
Mat ptMat(1, ptNum, CV_32SC2, pts + orgPtNum);
RotatedRect box = minAreaRect(ptMat);
memcpy(pts, orgPts, sizeof(CvPoint) * orgPtNum);
Mat ptMatTotal(1, orgPtNum+ ptNum, CV_32SC2, pts);
RotatedRect boxTotal = minAreaRect(ptMatTotal);
if (boxTotal.boundingRect().area() < orgBox.boundingRect().area() + box.boundingRect().area() * 1.5)
{
labels.push_back(id);
orgBox = boxTotal;
orgPtNum += ptNum;
CvPoint* temp = orgPts;
orgPts = pts;
pts = temp;
}
delete[] pts;
}
delete[] orgPts;
delete[] ids;
box = orgBox;
}
3.重新得到车牌区域的二值图像。
void SetBinaryImage(Mat& binary, Mat& lable, int labelsTotalNum, std::vector<int> plateLabels)
{
unsigned char* labelMap = new unsigned char[labelsTotalNum + 1];
memset(labelMap, 0xff, labelsTotalNum + 1);
for (unsigned int i = 0; i < plateLabels.size(); ++ i)
{
int id = plateLabels[i] + 1; //因为标记是从1开始的,而数组下标从0开始
labelMap[id] = 0x00;
}
unsigned char* bDat = binary.data;
memset(bDat, 0xff, binary.step * binary.rows);
int id;
for (int i = 0; i < binary.rows; ++ i)
{
int* lDat = (int*)(lable.ptr(i));
bDat = binary.ptr(i);
for (int j = 0; j < binary.cols; ++ j)
{
id = lDat[j];
if (id > 0)
bDat[j] = labelMap[id];
}
}
delete[] labelMap;
}
4.对图像进行归一化处理,用以解决图像大小不一的问题。得到大小相同的车牌区域的图像。
void NormalizeImage(Rect& rect, RotatedRect& box, Mat& binaryImg, Mat& plateImg,
RotatedRect& newBox, Mat& newBinaryImg, Mat& newPlateImg)
{
Rect areRect = box.boundingRect();
double ratio = 300.0 / areRect.width;
if (75.0 / areRect.height > ratio)
ratio = 75.0 / areRect.height;
int imgSize[2];
imgSize[0] = int(rect.height* ratio + 100.5f);
imgSize[1] = int(rect.width * ratio + 100.5f);
newBox.center.x = float(imgSize[1] / 2.0);
newBox.center.y = float(imgSize[0] / 2.0);
newBox.angle = box.angle;
newBox.size.width = float(box.size.width * ratio);
newBox.size.height= float(box.size.height * ratio);
Point2f srcPnts[3];
srcPnts[0].x = float(rect.x);
srcPnts[0].y = float(rect.y);
srcPnts[1] = srcPnts[0];
srcPnts[1].x += rect.width;
srcPnts[2] = srcPnts[0];
srcPnts[2].y += rect.height;
Point2f dstPnts[3];
dstPnts[0].x = 50;
dstPnts[0].y = 50;
dstPnts[1] = dstPnts[0];
dstPnts[1].x += float(rect.width * ratio);
dstPnts[2] = dstPnts[0];
dstPnts[2].y += float(rect.height* ratio);
Mat transMat = getAffineTransform(srcPnts, dstPnts); //有没有简单的方法求这个变换?
Size dstSize(imgSize[1], imgSize[0]);
newBinaryImg.create(2, imgSize, binaryImg.type());
newBinaryImg = Scalar(255, 0, 0);
warpAffine(binaryImg, newBinaryImg, transMat, dstSize, INTER_NEAREST, BORDER_TRANSPARENT);//是否可用resize函数替代
newPlateImg.create(2, imgSize, plateImg.type());
newPlateImg = Scalar(255, 255, 255);
warpAffine(plateImg, newPlateImg, transMat, dstSize, INTER_LINEAR, BORDER_TRANSPARENT); //是否可用resize函数替代
}
// Step2.2: normalize binary image blocks
//////////////////////////////////////////////////////////////////////////
void Dilate_ErodeBinaryImg(Mat& binaryImg)
{
int height= binaryImg.rows;
int width = binaryImg.cols;
Mat dist;
distanceTransform(binaryImg, dist, CV_DIST_L2, CV_DIST_MASK_PRECISE);
int i, j;
for (i = 0; i < height; ++ i)
{
float* distDat = (float*)(dist.ptr(i));
unsigned char* binDat = (unsigned char*)(binaryImg.ptr(i));
for (j = 0; j < width; ++ j)
{
if (distDat[j] < 7.5)
binDat[j] = 0;
}
}
dilate(binaryImg, binaryImg, Mat(), Point(-1, -1), 3);
}
5.通过图像匹配技术,得到车牌区域的透视投影下的校正图像。
bool RectifyPlateImage(Mat& Img, Mat& binary, RotatedRect& box, Mat& rectifiedImg)
{
Mat edge = binary.clone();
Canny(binary, edge, 50, 120, 5); //可用简单的方法来实现
Mat iedge;
edge.convertTo(iedge, -1, -1.0, 255);
//for debug
imwrite(_T("F:\\我的VC10应用程序\\plate num rec\\test\\test_contour_Img.bmp"), iedge);
Mat dist;
distanceTransform(iedge, dist, CV_DIST_L1, CV_DIST_MASK_PRECISE);
for (int i = 0; i < dist.rows; ++ i)
{
float* dat = dist.ptr<float>(i);
for (int j = 0; j < dist.cols; ++ j)
dat[j] = sqrt(dat[j]);
}
int w = Img.cols;
int h = Img.rows;
Point2f verts[4];
box.points(verts);
Point2f normalVerts[4];
GetMatchVertexs(verts, normalVerts);
normalVerts[0].x -= 5; normalVerts[0].y -= 5;
normalVerts[1].x += 5; normalVerts[1].y -= 5;
normalVerts[2].x += 5; normalVerts[2].y += 5;
normalVerts[3].x -= 5; normalVerts[3].y += 5;
FitRectangle(dist, normalVerts);
Point2f orgVerts[4];
float dd = 3;
orgVerts[0].x = 32 - dd; orgVerts[0].y = 32 - dd;
orgVerts[1].x =288 + dd; orgVerts[1].y = 32 - dd;
orgVerts[2].x =288 + dd; orgVerts[2].y = 96 + dd;
orgVerts[3].x = 32 - dd; orgVerts[3].y = 96 + dd;
Mat transMat = getPerspectiveTransform(normalVerts, orgVerts);
Size sizeDst(320, 128);
warpPerspective(Img, rectifiedImg, transMat, sizeDst);
return true;
}
void GetMatchVertexs(Point2f* verts, Point2f* normalVerts)
{
int indexs[4];
double maxMV = verts[0].x * verts[0].y;
double minMV = verts[0].x * verts[0].y;
double maxDV = verts[0].x / verts[0].y;
double minDV = verts[0].x / verts[0].y;
memset(indexs, 0x00, sizeof(int) * 4);
for (int i = 1; i < 4; ++ i)
{
double tempM = verts[i].x * verts[i].y;
double tempD = verts[i].x / verts[i].y;
if (tempM > maxMV)
{
maxMV = tempM;
indexs[2] = i;
}
else if (tempM < minMV)
{
minMV = tempM;
indexs[0] = i;
}
if (tempD > maxDV)
{
maxDV = tempD;
indexs[1] = i;
}
else if (tempD < minDV)
{
minDV = tempD;
indexs[3] = i;
}
}
for (int i = 0; i < 4; ++ i)
normalVerts[i] = verts[indexs[i]];
}
6.通过图像匹配技术,进一步分割得到各字符图像区域,
int GetBlueSpacePoints(Mat& templateImg, Point2d* templatePnts)
{
int h = templateImg.rows;
int w = templateImg.cols;
int cc = 0;
for (int i = 0; i < h; ++ i)
{
unsigned char* pDat = templateImg.ptr(i);
for (int j = 0; j < w; ++ j)
{
if (pDat[j] == 0)
{
templatePnts[cc].x = j;
templatePnts[cc].y = i;
++ cc;
}
}
}
return cc;
}
bool BlueSpaceMatch(Mat& image, Point2d* templatePnts, int nCount, double val, Point2d center,
double& scaleRatio, double& centerShiftX, double& centerShiftY)
{
double a_b[3];
double deltaX[3];
double AL[3];
double MM[9];
Point2d* pPntSet = new Point2d[nCount];
memcpy(pPntSet, templatePnts, sizeof(Point2d) * nCount);
double* pL = new double[nCount];
double* pA = new double[nCount * 3];
for (int j = 0; j < nCount; ++ j){
pPntSet[j].x -= center.x;
pPntSet[j].y -= center.y;
}
double va0, va1, va2, va3, va4;
double sx, sy;
double perror;
perror = 0.0f;
a_b[0] = scaleRatio;
a_b[1] = centerShiftX;
a_b[2] = centerShiftY;
for (int i = 0; i < 100; ++ i)
{
for (int j = 0; j < nCount; ++ j)
{
sx = a_b[0] * pPntSet[j].x + a_b[1];
sy = a_b[0] * pPntSet[j].y + a_b[2];
va0 = GetDataValue(image, sx, sy);
va1 = GetDataValue(image, sx + 0.5, sy);
va2 = GetDataValue(image, sx - 0.5, sy);
va3 = GetDataValue(image, sx, sy + 0.5);
va4 = GetDataValue(image, sx, sy - 0.5);
double dX = va1 - va2;
double dY = va3 - va4;
pA[j] = dX * pPntSet[j].x + dY * pPntSet[j].y;
pA[j + 1 * nCount] = dX;
pA[j + 2 * nCount] = dY; // A
pL[j] = val - va0; // L = G - G(')
}
double error = 0.0;
for(int j = 0; j < nCount; ++ j)
error += pL[j] * pL[j];
if(i == 0)
perror = error;
if (perror < error)
{
for (int i = 0; i < 3; ++ i)
{
deltaX[i] *= 0.5;
a_b[i] -= deltaX[i];
}
int j;
for(j = 0; j < 3; ++ j){
if(fabs(deltaX[j]) > 0.0000001)
break;
}
if(j == 3)
break;
continue;
}
perror = error;
int nOff_M = 0;
for(int j = 0; j < 3; ++ j){
for(int jj = 0; jj < 3; ++ jj){
MM[nOff_M + jj] = 0.0;
for(int kk = 0; kk < nCount; ++ kk)
MM[nOff_M + jj] += pA[j * nCount + kk] * pA[jj * nCount + kk];
} // M = A * A(T)
nOff_M += 3;
}
gslExInverseMatrix(MM, 3); // M = M(-)
int j;
for(j = 0; j < 3; ++ j){
AL[j] = 0.0; // AL = A * L
for(int kk = 0; kk < nCount; ++ kk)
AL[j] += pA[j * nCount + kk] * pL[kk];
}
for(j = 0; j < 3; ++ j){
deltaX[j] = 0.0;
for(int jj = 0; jj < 3; ++ jj)
deltaX[j] += MM[j * 3 + jj] * AL[jj];
}
// deltaX = (A * A(T))(-) * A(T) * L
for(j = 0; j < 3; j++){
if(fabs(deltaX[j]) > 0.0000001)
break;
}
if(j == 3)
break;
for(j = 0; j < 3; j++)
a_b[j] += deltaX[j]; // a = a + delta / b = b + delta
}
delete[] pL;
delete[] pA;
scaleRatio = a_b[0];
centerShiftX = a_b[1];
centerShiftY = a_b[2];
delete [] pPntSet;
return true;
}
int GetCharBlockRect(double& scaleRatio, double& centerShiftX, double& centerShiftY, Rect* rects)
{
Size templateImgSize;
templateImgSize.width = 256;
templateImgSize.height= 64;
//
Point2d center;
center.x = templateImgSize.width / 2;
center.y = templateImgSize.height/ 2;
Rect templateBlockRect[7];
int xx[7] = {1, 36, 85, 120, 155, 190, 225};
for (int i = 0; i < 7; ++ i)
{
templateBlockRect[i].x = xx[i];
templateBlockRect[i].y = 4;
templateBlockRect[i].width = 29;
templateBlockRect[i].height= 57;
}
//
//
for (int i = 0; i < 7; ++ i)
{
templateBlockRect[i].x -= int(center.x + 0.5f);
templateBlockRect[i].y -= int(center.y + 0.5f);
rects[i].x = int(templateBlockRect[i].x * scaleRatio + centerShiftX + 0.5f);
rects[i].y = int(templateBlockRect[i].y * scaleRatio + centerShiftY + 0.5f);
rects[i].width = int(templateBlockRect[i].width * scaleRatio + 0.5f);
rects[i].height= int(templateBlockRect[i].height* scaleRatio + 0.5f);
}
return 7;
}
int ExtractCharBlockImg(Mat& plateImg, Rect* rects, Mat* blockImgs)
{
Size dstSize;
dstSize.width = 30;
dstSize.height= 60;
for (int i = 0; i < 7; ++ i)
{
int size[2];
size[0] = dstSize.width;
size[1] = dstSize.height;
blockImgs[i].create(2, size, plateImg.type());
Mat temp(plateImg, rects[i]);
resize(temp, blockImgs[i], dstSize);
}
return 7;
}
7.通过图像匹配技术,识别各个字符。
int RecognizeWord(InputArray blockImg);
int RecognizeChar(InputArray blockImg);
int RecognizeNumChar(InputArray blockImg);
int RecognizeBlockImg(Mat& blockImg, int i)
{
if (i < 0 || i > 6)
return -1;
Mat grayImg;
cvtColor(blockImg, grayImg, CV_RGB2GRAY);
Mat grayImgInv;
grayImg.convertTo(grayImgInv, -1, -1.0, 255);
Mat binaryImg;
adaptiveThreshold(grayImgInv, binaryImg, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 51, 9);
Mat binaryImgSmooth;
GaussianBlur(binaryImg, binaryImgSmooth, Size(3, 3), 1.25f, 1.25f);
imwrite(_T("F:\\我的VC10应用程序\\plate num rec\\test\\test_block_binary_char.bmp"), binaryImgSmooth);
int nRes = -1;
if (i == 0)
nRes = RecognizeWord(binaryImgSmooth);
else if (i == 1)
nRes = RecognizeChar(binaryImgSmooth);
else
nRes = RecognizeNumChar(binaryImgSmooth);
return nRes;
}
void MatchTemplatesWord(InputArray templatsImg_, InputArray charImg_, std::vector<double>& results);
void MatchTemplatesChar(InputArray templatsImg_, InputArray charImg_, std::vector<double>& results);
void MatchTemplatesNumChar(InputArray templatsImg_, InputArray numImg_, std::vector<double>& results);
int RecognizeWord(InputArray blockImg)
{
Mat tempImg = imread(_T("..\\template\\word_grayScale_AutoSize_Smooth.bmp"), CV_LOAD_IMAGE_GRAYSCALE);
std::vector<double> results;
MatchTemplatesWord(tempImg, blockImg, results);
double minVal = results[0];
unsigned int nl = 0;
for (unsigned int i = 1; i < results.size(); ++ i)
{
if (results[i] < minVal)
{
minVal = results[i];
nl = i;
}
}
return nl;
}
int RecognizeChar(InputArray blockImg)
{
Mat tempImg = imread(_T("..\\template\\char_grayScale_AutoSize_Smooth.bmp"), CV_LOAD_IMAGE_GRAYSCALE);
std::vector<double> results;
MatchTemplatesChar(tempImg, blockImg, results);
double minVal = results[0];
unsigned int nl = 0;
for (unsigned int i = 1; i < results.size(); ++ i)
{
if (results[i] < minVal)
{
minVal = results[i];
nl = i;
}
}
return nl;
}
int RecognizeNumChar(InputArray blockImg)
{
Mat tempImg = imread(_T("..\\template\\num_char_grayScale_AutoSize_Smooth.bmp"), CV_LOAD_IMAGE_GRAYSCALE);
std::vector<double> results;
MatchTemplatesNumChar(tempImg, blockImg, results);
double minVal = results[0];
unsigned int nl = 0;
for (unsigned int i = 1; i < results.size(); ++ i)
{
if (results[i] < minVal)
{
minVal = results[i];
nl = i;
}
}
return nl;
}
代码比较多,有部分不影响主线的代码就没有贴出来了。如果大家有兴趣讨论可以私信我哈。秋秋31667460