基于OpenCV技术的汽车牌照的识别

author: RoWe98(RexRowe)

OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列
C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
车牌识别技术要求能够将运动中的汽车牌照从复杂背景中提取并识别出来,通过车牌提取、图像预处理、特征提取、车牌字符识别等技术,识别车辆牌号、颜色等信息,目前最新的技术水平为字母和数字的识别率可达到99.7%,汉字的识别率可达到99%。
车牌识别技术结合电子不停车收费系统(ETC)识别车辆,过往车辆通过道口时无须停车,即能够实现车辆身份自动识别、自动收费。在车场管理中,为提高出入口车辆通行效率,车牌识别针对无需收停车费的车辆(如月卡车、内部免费通行车辆),建设无人值守的快速通道,免取卡、不停车的出入体验,正改变出入停车场的管理模式。

设计平台及工具

1.Visual Studio 2017

2.OpenCV2.4.9

3.C++

设计原理

车牌自动识别是一项利用车辆的动态视频或静态图像进行牌照号码、牌照颜色自动识别的模式识别技术。其硬件基础一般包括触发设备(监测车辆是否进入视野)、摄像设备、照明设备、图像采集设备、识别车牌号码的处理机(如计算机)等,其软件核心包括车牌定位算法、车牌字符分割算法和光学字符识别算法等。某些车牌识别系统还具有通过视频图像判断是否有车的功能称之为视频车辆检测。一个完整的车牌识别系统应包括车辆检测、图像采集、车牌识别等几部分(如图1所示)。当车辆检测部分检测到车辆到达时触发图像采集单元,采集当前的视频图像。车牌识别单元对图像进行处理,定位出牌照位置,再将牌照中的字符分割出来进行识别,然后组成牌照号码输出。

主要框架:

avatar

图像的预处理

车牌定位的第一步为图像预处理。为了方便计算,系统通常将获取的图片灰度化。将彩色图像转化成为灰度图像的过程就称为图像的灰度化处理。彩色图像中R、G、B三个分量的值决定了具体的像素点。一个像素点可以有上千万种颜色。而灰度图像是一种彩色图像,但是它的特点在于R、G、B三个分量具体的值是一致的。灰度图中每个像素点的变化区间是0到255,由于方便计算,所以在实际工程处理中会先将各种格式的图像转变成灰度图像。在保留图像轮廓和特征的基础上,灰度图仍然能够反映整幅图像轮廓和纹理。在Opencv里面有实现图像灰度化的接口。调用OpenCV中的cvSmooth函数进行中值滤波处理,以去除细小毛刺。

图像二值化

局部自适应二值化是针对灰度图像中的每一个像素逐点进行阈值计算,它的阈值是由像素周围点局部灰度特性和像素灰度值来确定的。局部阈值法是逐个计算图像的每个像素灰度级,保存了图像的细节信息,非均匀光照条件等情况虽然影响整个图像的灰度分布缺不影响局部的图像性质,但也存在缺点和问题,相比全局阈值法来说,它的计算时间较长,但适用于多变的环境。
设图像在像素点(x,y)处的灰度值为f(x,y),考虑以像素点(x,y)为中心的(2w+1)*(2w+1)窗口(w为窗口宽度),则局部自适应二值化算法可以描述如下:

a.计算图像中各点(x,y)的阈值w(x,y)

W(x,y)=0.5*(max f(x+m,y+n)+min f(x+m,y+n))

b.如果f(x,y)>w(x,y),则二值化结果为1,代表字符区域的目标点;否则二值化结果为0,代表背景区域的目标点。

车牌位置的定位

车牌细定位的目的是为下一步字符的分割做,就是要进一步去掉车牌冗余的部分。在一幅经过适当二值化处理 含有车牌的图像中,车牌区域具有以下三个基本特征:
列在一个不大的区域内密集包含有多个字符;
车牌字符与车牌底色形成强烈对比;
车牌区域大小相对固定,区域长度和宽度成固定比例。
根据以上特征,车牌区域所在行相邻像素之间0 到1和1到0 的的变化会很频繁,变化总数会大于一个临界值,这可以作为寻找车牌区域的一个依据。 因此根据跳变次数与设定的阈值比较,就可以确定出车牌的水平区域。
由于车牌一般悬挂在车辆下部,所以采用从上到下,从左到右的方式对图像进行扫描。车牌的字符部分由7个字符数与两个竖直边框组成,则车牌区域内任一行的跳变次数至少为(7+2)2=18次。从图像的底部开始向顶部进行扫描,则第一组连续数行且每行的跳变次数都大于跳变阈值,同时满足连续行数大于某个阈值。
在车牌的水平区域中,最高行与最低行的差值即为车牌在图像中的高度。我国车牌区域为矩形,宽高比约为3.14,取3.14
H作为车牌的宽度。在水平区域内选择任意一行,用L长的窗口由左至右移动,统计窗口中相邻像素0,1的跳变次数并存入数组中。若窗口移动到车牌的垂直区域时,窗口内的跳变次数应该最大。因此在数组中找到最大值,其对应的区域即为车牌的垂直区域。

车牌字符的识别

车牌字符识别是字符识别的重要组成部分。车牌字符识别的最终目的就是将图像中的车牌字符转化成文本字符,车牌字符的识别属于印刷体识别范畴。
字符识别的基本思想是匹配判别。抽取待识别字符特征按照字符识别的原理和预先存储在计算机中的标准字符模式表达形式的集合逐一进行匹配,找出最接近输入字符模式的表达形式,该表达形式对应的字就是识别结果。

字符识别的原理如下:

avatar

根据我国的车牌牌照标准,车牌第一位字符一般为汉字,车牌第二位英文大写字母,第三位至第七位为英文大写字母或数字。考虑到神经网络对小类别字符集有较高的识别率,因此在车牌字符识别系统中,分别设计三个神经网络:汉字网络、字母网络、字母数字网络 实现对字符的分类识别。

avatar

程序代码

1.OpencvCar.h 车牌识别类

程序

#pragma once
#pragma comment (lib, "cxcore.lib")
#pragma comment (lib, "cv.lib")
#pragma comment (lib, "ml.lib")
#pragma comment (lib, "cvaux.lib")
#pragma comment (lib, "highgui.lib")
#pragma comment (lib, "cvcam.lib")
#include "cv.h"
#include "highgui.h"
#include "iostream"
#include "string"
using namespace std;
#define DEBUGTEST
#define POINT_X  18                                           // 水平方向不重点                        
#define POINT_Y  2                                            // 垂直方向不重点                            
#define WITH_X 0.1                                            // 水平方向车牌在域     
#define WITH_Y 0.2                                            // 垂直方向车牌在域     
#define HIGH_WITH_CAR   4.8   //(440/85)               // 小车的宽高比  440/140    
#define CHARACTER 15
#define TEMPLETENUM  43
class OpencvCar
{
public:
    OpencvCar();
    int CodeRecognize(IplImage *imgTest, int num, int char_num); //字符识别
    int SegmentPlate();                                //字符分割
    int PlateAreaSearch(IplImage *pImg_Image, IplImage *src);        //车牌定位
    void Threshold(IplImage *Image, IplImage *Image_O);    //二值化
    int AdaptiveThreshold(int t, IplImage *Image);        //自适应阈值法
protected:
    IplImage *src;                                // 原始图片
    IplImage *pImgCanny;                      //二值化的图
    IplImage *pImgResize;                     //归一化的车牌区域灰度图
    IplImage *pImgCharOne;                // 字符图片
    IplImage *pImgCharTwo;
    IplImage *pImgCharThree;
    IplImage *pImgCharFour;
    IplImage *pImgCharFive;
    IplImage *pImgCharSix;
    IplImage *pImgCharSeven;
};

2.OpencvCar.cpp 车牌识别类实现程序

程序

#include "OpencvCar.h"

// 车牌字符模板特征值
const int Num_Templete[TEMPLETENUM][CHARACTER] =
{
    { 16, 19, 10, 12, 10, 10, 15, 18, 110, 3, 2, 2, 3, 3, 3 },     //0
    { 9, 11, 10, 10, 10, 10, 9, 10, 79, 2, 2, 2, 0, 2, 12 },       //1
    { 18, 19, 3, 18, 10, 10, 23, 22, 123, 4, 2, 2, 7, 6, 8 },      //2
    { 19, 21, 11, 14, 4, 20, 18, 22, 129, 2, 2, 4, 6, 6, 7 },      //3
    { 2, 18, 11, 22, 20, 21, 11, 18, 123, 2, 4, 2, 6, 7, 5 },      //4
    { 23, 19, 20, 12, 9, 20, 18, 22, 143, 2, 4, 4, 6, 6, 6 },      //5
    { 6, 13, 17, 8, 15, 20, 18, 20, 117, 2, 2, 4, 5, 7, 6 },       //6
    { 21, 21, 0, 20, 8, 12, 9, 11, 102, 2, 2, 2, 2, 8, 15 },       //7
    { 17, 18, 18, 19, 14, 20, 17, 20, 143, 4, 2, 4, 6, 6, 6 },     //8
    { 16, 18, 15, 21, 7, 19, 13, 7, 116, 3, 2, 2, 6, 6, 5 },       //9
    { 10, 10, 16, 16, 20, 20, 18, 19, 129, 2, 4, 2, 8, 3, 6 },     //A
    { 24, 20, 20, 19, 22, 22, 24, 20, 171, 4, 8, 4, 6, 6, 6 },     //B
    { 18, 19, 20, 4, 20, 8, 17, 21, 127, 3, 2, 4, 4, 4, 4 },       //C
    { 23, 19, 11, 20, 12, 20, 22, 21, 148, 3, 3, 3, 4, 4, 4 },     //D
    { 23, 19, 21, 9, 22, 8, 23, 23, 148, 2, 2, 2, 6, 6, 6 },       //E
    { 25, 17, 20, 9, 22, 8, 19, 0, 120, 2, 2, 2, 4, 4, 4 },        //F
    { 17, 18, 22, 14, 12, 24, 18, 21, 146, 4, 7, 4, 4, 6, 6 },     //G
    { 14, 20, 18, 22, 17, 22, 16, 20, 149, 4, 1, 4, 2, 2, 2 },     //H
    { 0, 17, 0, 20, 3, 20, 18, 22, 100, 2, 2, 4, 2, 2, 2 },        //J
    { 19, 20, 26, 10, 20, 20, 20, 22, 157, 4, 4, 4, 3, 5, 11 },    //K
    { 20, 0, 20, 0, 20, 0, 25, 20, 105, 2, 2, 2, 2, 2, 2 },        //L
    { 20, 10, 27, 17, 20, 10, 22, 14, 140, 1, 3, 3, 4, 1, 5 },     //M
    { 21, 12, 25, 17, 26, 12, 18, 18, 149, 3, 5, 3, 5, 5, 6 },     //N 
    { 23, 19, 18, 20, 21, 8, 22, 0, 131, 3, 3, 2, 4, 4, 4 },       //P
    { 18, 19, 20, 10, 26, 15, 18, 21, 147, 3, 3, 4, 5, 7, 5 },     //Q
    { 26, 19, 21, 18, 21, 17, 20, 21, 163, 4, 3, 4, 4, 6, 5 },     //R
    { 18, 18, 18, 10, 8, 17, 17, 22, 128, 4, 3, 4, 6, 6, 6 },      //S
    { 22, 18, 10, 10, 10, 10, 10, 10, 100, 2, 2, 2, 33, 2, 2 },    //T
    { 18, 12, 20, 10, 20, 10, 19, 21, 130, 3, 3, 3, 2, 2, 2 },     //U
    { 20, 19, 20, 20, 15, 14, 9, 10, 127, 4, 4, 2, 9, 1, 8 },      //V
    { 21, 25, 26, 28, 16, 16, 21, 19, 172, 6, 2, 4, 13, 0, 7 },    //W
    { 21, 21, 13, 13, 12, 11, 22, 21, 134, 4, 2, 4, 8, 0, 10 },    //X
    { 21, 20, 10, 11, 10, 10, 10, 11, 103, 3, 2, 2, 5, 2, 6 },     //Y
    { 21, 23, 5, 15, 15, 5, 24, 20, 128, 2, 2, 2, 8, 8, 7 },       //Z
    { 13, 14, 10, 10, 10, 10, 13, 13, 93, 2, 2, 2, 29, 2, 29 },    //I
    { 20, 20, 13, 20, 19, 12, 17, 20, 141, 3, 3, 4, 4, 4, 4 },     //O          //36
    { 14, 15, 17, 17, 16, 10, 25, 24, 138, 0, 2, 4, 12, 8, 9 },    //云        //37
    { 17, 20, 17, 12, 33, 28, 23, 20, 170, 3, 4, 7, 13, 6, 4 },    //苏
    { 21, 21, 23, 24, 24, 25, 31, 27, 196, 0, 9, 6, 8, 6, 7 },     //京
    { 19, 27, 20, 34, 19, 36, 24, 37, 216, 4, 4, 7, 13, 28, 3 },   //湘
    { 17, 14, 23, 27, 36, 40, 26, 27, 210, 4, 13, 4, 16, 14, 14 }, //鲁
    { 24, 24, 32, 38, 34, 32, 17, 22, 223, 9, 6, 10, 11, 12, 9 }, // 粤
    { 22, 20, 33, 37, 25, 24, 24, 25, 210, 13, 3, 6, 12, 8, 7 }     //蒙
};

//车牌字符
const char *PlateCode[TEMPLETENUM] =
{
    "0", "1", "2", "3", "4",
    "5", "6", "7", "8", "9",
    "A", "B", "C", "D", "E",
    "F", "G", "H", "J", "K",
    "L", "M", "N", "P", "Q",
    "R", "S", "T", "U", "V",
    "W", "X", "Y", "Z", "I", "O",
    "云", "苏", "京", "湘", "鲁", "粤", "蒙"
};
const char *G_PlateChar[7] = { "没", "有", "找", "到", "车", "牌", "!" }; // 车牌号
OpencvCar::OpencvCar()
{
    //初始化  变量
    IplImage *src = NULL;      //原始图片                        
    IplImage *pImgCanny = NULL;  //二值化的图                    
    IplImage *pImgResize = NULL;   //归一化的车牌区域灰度图                  
    IplImage *pImgCharOne = NULL;   //字符图片             
    IplImage *pImgCharTwo = NULL;
    IplImage *pImgCharThree = NULL;
    IplImage *pImgCharFour = NULL;
    IplImage *pImgCharFive = NULL;
    IplImage *pImgCharSix = NULL;
    IplImage *pImgCharSeven = NULL;
}
/* ----------------------自适应阈值法---------------------------------//
//--input: t:中心阈值
//--                Image: 图片指针
//-- output:  return   自适应均值的阈值
//-------------------------------------------------------------------------*/
int OpencvCar::AdaptiveThreshold(int t, IplImage *Image)
{
    int t1 = 0, t2 = 0, tnew = 0, i = 0, j = 0;
    int Allt1 = 0, Allt2 = 0, accountt1 = 0, accountt2 = 0;//Allt1 Allt2 保存两部分的和 
    for (j = 0; j < Image->height; j++) //根据现有T,将图像分为两部分,分别求两部分的平均值t1、t2
    {
        for (i = 0; i < Image->width; i++)
        {
            if (CV_IMAGE_ELEM(Image, uchar, j, i) < t)
            {
                Allt1 += CV_IMAGE_ELEM(Image, uchar, j, i);
                accountt1++;
            }
            else
            {
                Allt2 += CV_IMAGE_ELEM(Image, uchar, j, i);
                accountt2++;
            }
        }
    }
    t1 = Allt1 / accountt1;
    t2 = Allt2 / accountt2;
    tnew = 0.5*(t1 + t2);
    if (tnew == t) //若t1、t2的平均值和t相等,则阈值确定
        return tnew;
    else
        AdaptiveThreshold(tnew, Image); //不等则以t1、t2的平均值为新阈值迭代
}
/* -----------------------二值化-----------------------------------------//
// --Input:
//              IplImage *Image: 图片指针
//             int AdaptiveThreshold(int t,IplImage *Image)  //自适应阈值法
//-- Output:
//              IplImage *Image_O  二值化后的图片
//--  Description:
//              采用Canny边缘检测二值化
//-------------------------------------------------------------------------*/
void OpencvCar::Threshold(IplImage *Image, IplImage *Image_O)
{
    //得到图片的最大灰度值和最小灰度值
    int thresMax = 0, thresMin = 255, i = 0, j = 0, t = 0;
    for (j = 0; j < Image->height; j++)
        for (i = 0; i < Image->width; i++)
        {
            if (CV_IMAGE_ELEM(Image, uchar, j, i) > thresMax) //像素值 大于 255  
                thresMax = CV_IMAGE_ELEM(Image, uchar, j, i);  //把元素值赋给 thresMax
            else if (CV_IMAGE_ELEM(Image, uchar, j, i) < thresMin) //如果小于 0   
                thresMin = CV_IMAGE_ELEM(Image, uchar, j, i); //则 改变thresMin
        }
    //int T=(thresMax+thresMin)*0.5; //灰度的最大值和最小值的平均
    cvCanny(Image, Image_O, AdaptiveThreshold((thresMax + thresMin)*0.5, Image), thresMax*0.7, 3);
    //小阈值用来控制边缘连接  大阈值用来控制强边缘的初始化分割  cvCanny只接受单通道的输入
}
//车牌区域检测
/***************************************************
INPUT  : pImg_Image   二值化后的图像
src         原始图像
OUTPUT :  pImgResize  归一化后的车牌灰度图像
Description :
输出归一化图片大小 40*20
归一化的区域为在在原始图片(src) 归一化之后,在再二值化后 图
定位方法:  水平分割,垂直分割,归一化。
***********************************************************/
int OpencvCar::PlateAreaSearch(IplImage *pImg_Image, IplImage *src)
{
    if (pImg_Image == NULL) { return 0; } // 检测是否有值

    IplImage* imgTest = 0;
    int i = 0, j = 0, k = 0, m = 0;
    bool flag = 0;
    int plate_n = 0, plate_s = 0, plate_e = 0, plate_w = 0;  //关于车牌的一些变量

    int *num_h = new int[max(pImg_Image->width, pImg_Image->height)];
    if (num_h == NULL)
    {
        cout << "memory exhausted" << endl;
        //MessageBox("memory exhausted!");
        return 0;
        //    exit(1);    
    }  // end if 

    for (i = 0; i < pImg_Image->width; i++) { num_h[i] = 0; }  // 初始化 分配的空间

    imgTest = cvCreateImage(cvSize(pImg_Image->width, pImg_Image->height), IPL_DEPTH_8U, 1);
    cvCopy(pImg_Image, imgTest);

    //-- 水平 轮廓细化
    for (j = 0; j < imgTest->height; j++)
    {
        for (i = 0; i < imgTest->width - 1; i++)
        {
            CV_IMAGE_ELEM(imgTest, uchar, j, i) = CV_IMAGE_ELEM(imgTest, uchar, j, i + 1) - CV_IMAGE_ELEM(imgTest, uchar, j, i);
            num_h[j] += CV_IMAGE_ELEM(imgTest, uchar, j, i) / 250;
        }
    }


    int temp_1 = 0;
    int temp_max = 0;
    int temp_i = 0;  //说明这里for 循环 是找出数据量最大的地方  20行  也即是 车牌区域
    for (j = 0; j < imgTest->height - 20; j++)
    {
        temp_1 = 0;
        for (i = 0; i < 20; i++)//此处for循环 是为了计算20行的总数据量  
            temp_1 += num_h[i + j];
        if (temp_1 >= temp_max)
        {
            temp_max = temp_1;
            temp_i = j;//记录20行的最大数据量的开始 行
        }
    }
    k = temp_i;//以下两个while 循环是为了找出 车牌的上下边界  当一行的数据量小于某个数值时 设定此为分界行
    while (((num_h[k + 1] > POINT_X) || (num_h[k + 2] > POINT_X) || (num_h[k] > POINT_X)) && k) k--;//找出行边界行
    plate_n = k + 1;//k+2; 
    k = temp_i + 10;
    while (((num_h[k - 1] > POINT_X) || (num_h[k - 2] > POINT_X) || (num_h[k] > POINT_X)) && (k < imgTest->height)) k++; //找出下边界行
    plate_s = k;//k-2; 

    // 没找到水平分割线,设置为默认值
    if (!(plate_n && plate_s  //行为负值 或者 上行大于下行  或者 车牌宽度大于 设定值 则水平分割失败
        && (plate_n < plate_s) && ((plate_s - plate_n)*HIGH_WITH_CAR < imgTest->width*(1 - WITH_X))))
    {
        //flag=1;
        cout << "水平分割失败" << endl;
        //MessageBox("水平分割失败!");
        return 0;
    }
    else//找到水平线  
    {
        int  max_count = 0;
        int  plate_length = (imgTest->width - (plate_s - plate_n)*HIGH_WITH_CAR);
        plate_w = imgTest->width*WITH_X - 1;//车牌宽度 默认

        //--垂直方向
        for (i = 0; i < imgTest->width; i++)
            for (j = 0; j < imgTest->height - 1; j++)//用的方法是 差分赋值法——我起的名字 应该是为了细化 
            {
                CV_IMAGE_ELEM(imgTest, uchar, j, i) = CV_IMAGE_ELEM(imgTest, uchar, j + 1, i) - CV_IMAGE_ELEM(imgTest, uchar, j, i);
            }
        //下面这一段代码 相当于 拿一个车牌大小的矩形区域 从左往右 滑动,什么时候圈住的数据量最大的时候
        // 就代表找到的车牌的左边界 此时 车牌左边界的横坐标是 k
        //这里 plate_length  有点难理解,它的值是 原图像的宽度减去车牌的宽度差值。
        for (k = 0; k < plate_length; k++)
        {
            for (i = 0; i < (int)((plate_s - plate_n)*HIGH_WITH_CAR); i++)
                for (j = plate_n; j < plate_s; j++)//两水平线之间 
                {
                    num_h[k] = num_h[k] + CV_IMAGE_ELEM(imgTest, uchar, j, (i + k)) / 250;
                }
            if (num_h[k] > max_count)
            {
                max_count = num_h[k];
                plate_w = k;
            }  // end if 

        }

        CvRect ROI_rect;                 //获得图片感兴趣区域
        ROI_rect.x = plate_w;
        ROI_rect.y = plate_n;
        ROI_rect.width = (plate_s - plate_n)*HIGH_WITH_CAR;
        ROI_rect.height = plate_s - plate_n;

        if ((ROI_rect.width + ROI_rect.x) > pImg_Image->width)
        {
            ROI_rect.width = pImg_Image->width - ROI_rect.x;
            cout << "垂直方向分割失败!";
            //MessageBox("垂直方向分割失败!");
            return 0;
        }
        else
        {
            IplImage *pImg8uROI = NULL;         //感兴趣的图片
            pImg8uROI = cvCreateImage(cvSize(ROI_rect.width, ROI_rect.height), src->depth, src->nChannels);
            IplImage *pImg8u11 = NULL;        //车牌区域灰度图
            pImg8u11 = cvCreateImage(cvSize(40 * HIGH_WITH_CAR, 40), pImg8uROI->depth, pImg8uROI->nChannels);

            cvSetImageROI(src, ROI_rect);//将 ROI_rect 设置为感兴趣区域
            cvCopy(src, pImg8uROI, NULL);//把感兴趣区域 复制到 pImg8uROI
            cvResetImageROI(src);   //重新设置感兴趣区域

            pImgResize = cvCreateImage(cvSize(40 * HIGH_WITH_CAR, 40), IPL_DEPTH_8U, 1);
            cvResize(pImg8uROI, pImg8u11, CV_INTER_LINEAR); //线性插值  归一化  把车牌变成统一大小

            cvCvtColor(pImg8u11, pImgResize, CV_RGB2GRAY);  //  转为灰度图      Y=0.299*R + 0.587*G + 0.114*B
            Threshold(pImgResize, pImgResize);             // 二值化

            cvReleaseImage(&pImg8uROI);
            cvReleaseImage(&pImg8u11);
            cvReleaseImage(&imgTest);

            cvNamedWindow("车牌");
            cvShowImage("车牌", pImgResize);
        }   // end if 
    }    // end if 

    // 释放内存
    delete[]num_h;
    num_h = NULL;
    return 1;
}
//* -----------------------字符分割-----------------------------------------//
// --Input: 
//              IplImage * pImgResize : 归一化的车牌区域灰度图   
//-- Output: 
//               IplImage *pImgCharOne              // 字符图片
//               IplImage *pImgCharTwo
//               IplImage *pImgCharThree
//               IplImage *pImgCharFour
//               IplImage *pImgCharFive
//               IplImage *pImgCharSix
//               IplImage *pImgCharSeven
//--  Description:  
//               利用垂直投影法和车牌的特征结合分割字符区域
//-------------------------------------------------------------------------*/  

int OpencvCar::SegmentPlate()
{
    if (pImgResize == NULL) { return 0; } // 没有切割成功,直接弹出

    int *num_h = new int[max(pImgResize->width, pImgResize->height)];     // 开辟空间  一般应该是 width 大小
    if (num_h == NULL)   // 
    {
        //cout<<"memory exhausted"< 0; j--)//从第二个字符的末端开始 往前找第一个和第二个字符起始位置
    {
        if ((num_h[j] < POINT_Y) && (num_h[j - 1] < POINT_Y))//只要有两个列的 17到24 行的值小于 2,
        {                                           //即找到第二个字符的开始位置
            letter[2] = j;      // find letter[2]  第二个字符的开始位置
            letter[1] = (j >= 23) ? j - 3 : letter[1];   //第一个字符的结束位置
            letter[0] = (j >= 23) ? j - 23 : letter[0];  //第一个字符的起始位置
            break;       //找到就退出循环
        }
    }

    j = 2;  flag = 0; flag1 = 0;//两个标记
    for (i = letter[4]; i < 40 * HIGH_WITH_CAR; i++)  //从第三个字符的开始位置算起
    {
        if ((num_h[i] > POINT_Y) && (num_h[i - 1] > POINT_Y) && !flag)
        {
            flag = 1;
            flag1 = 0;
            letter[2 * j] = i - 1; //这里 只记录字符的开始位置
            if (j == 6)  //判断 最后一个字符的结束位置 是否越界 超出界限,如果没有,则letter[13]=letter[12]+20
            {
                letter[2 * j + 1] = ((letter[2 * j] + 20) > 40 * HIGH_WITH_CAR - 1) ? 40 * HIGH_WITH_CAR - 1 : letter[2 * j] + 20;
                break;//退出 for循环
            }
        }
        else if ((num_h[i] < POINT_Y) && (num_h[i - 1] < POINT_Y) && !flag1 && flag)//如果是 空白区域  
        {
            flag = 0;
            flag1 = 1;
            letter[2 * j + 1] = i - 1;
            j++; //j自动加 1
        }                        //    1    
    }
    // 删除角点                              1   0   1
    for (i = 0; i < 40 * HIGH_WITH_CAR - 1; i++)
    {                                   //      1    删除角点  相当于拿一个半径为1 的圆 去圈 如果四周有两个是1  则自己设置为0   

        for (j = 0; j < 39; j++)  // 0-16 /40
        {

            if (CV_IMAGE_ELEM(pImgResize, uchar, j, i) && CV_IMAGE_ELEM(pImgResize, uchar, j, i + 1) && CV_IMAGE_ELEM(pImgResize, uchar, j + 1, i)) //  01
                CV_IMAGE_ELEM(pImgResize, uchar, j, i) = 0;                                                                                                                                                                                                                             //   1

            if (CV_IMAGE_ELEM(pImgResize, uchar, j, i) && CV_IMAGE_ELEM(pImgResize, uchar, j, i - 1) && CV_IMAGE_ELEM(pImgResize, uchar, j + 1, i))     //  10
                CV_IMAGE_ELEM(pImgResize, uchar, j, i) = 0;                                                                                                                                                                                                                               //    1                      

            if (CV_IMAGE_ELEM(pImgResize, uchar, j, i) && CV_IMAGE_ELEM(pImgResize, uchar, j, i - 1) && CV_IMAGE_ELEM(pImgResize, uchar, j - 1, i))         //   1      
                CV_IMAGE_ELEM(pImgResize, uchar, j, i) = 0;                                                                                    // 10
            if (CV_IMAGE_ELEM(pImgResize, uchar, j, i) && CV_IMAGE_ELEM(pImgResize, uchar, j, i + 1) && CV_IMAGE_ELEM(pImgResize, uchar, j - 1, i))           // 1      
                CV_IMAGE_ELEM(pImgResize, uchar, j, i) = 0;                                                                                 // 01
        }
    }

    // 分割出字符图片
    pImgCharOne = cvCreateImage(cvSize(20, 40), IPL_DEPTH_8U, 1);
    pImgCharTwo = cvCreateImage(cvSize(20, 40), IPL_DEPTH_8U, 1);
    pImgCharThree = cvCreateImage(cvSize(20, 40), IPL_DEPTH_8U, 1);
    pImgCharFour = cvCreateImage(cvSize(20, 40), IPL_DEPTH_8U, 1);
    pImgCharFive = cvCreateImage(cvSize(20, 40), IPL_DEPTH_8U, 1);
    pImgCharSix = cvCreateImage(cvSize(20, 40), IPL_DEPTH_8U, 1);
    pImgCharSeven = cvCreateImage(cvSize(20, 40), IPL_DEPTH_8U, 1);

    CvRect ROI_rect1;
    ROI_rect1.x = 0.5*(letter[1] + letter[0]) - 10; //
    ROI_rect1.y = 0;
    ROI_rect1.width = 20;
    ROI_rect1.height = 40;
    cvSetImageROI(pImgResize, ROI_rect1);
    cvCopy(pImgResize, pImgCharOne, NULL); //获取第1个字符
    cvResetImageROI(pImgResize);
    cvNamedWindow("第一个字符");
    cvShowImage("第一个字符", pImgCharOne);

    ROI_rect1.x = 0.5*(letter[3] + letter[2]) - 10;
    ROI_rect1.y = 0;
    ROI_rect1.width = 20;
    ROI_rect1.height = 40;
    cvSetImageROI(pImgResize, ROI_rect1);
    cvCopy(pImgResize, pImgCharTwo, NULL); //获取第2个字符
    cvResetImageROI(pImgResize);
    cvNamedWindow("第二个字符");
    cvShowImage("第二个字符", pImgCharTwo);

    ROI_rect1.x = 0.5*(letter[5] + letter[4]) - 10;
    ROI_rect1.y = 0;
    ROI_rect1.width = 20;
    ROI_rect1.height = 40;
    cvSetImageROI(pImgResize, ROI_rect1);
    cvCopy(pImgResize, pImgCharThree, NULL); //获取第3个字符
    cvResetImageROI(pImgResize);
    cvNamedWindow("第三个字符");
    cvShowImage("第三个字符", pImgCharThree);

    ROI_rect1.x = 0.5*(letter[7] + letter[6]) - 10;
    ROI_rect1.y = 0;
    ROI_rect1.width = 20;
    ROI_rect1.height = 40;
    cvSetImageROI(pImgResize, ROI_rect1);
    cvCopy(pImgResize, pImgCharFour, NULL); //获取第4个字符
    cvResetImageROI(pImgResize);
    cvNamedWindow("第四个字符");
    cvShowImage("第四个字符", pImgCharFour);

    ROI_rect1.x = 0.5*(letter[9] + letter[8]) - 10;
    ROI_rect1.y = 0;
    ROI_rect1.width = 20;
    ROI_rect1.height = 40;
    cvSetImageROI(pImgResize, ROI_rect1);
    cvCopy(pImgResize, pImgCharFive, NULL); //获取第5个字符
    cvResetImageROI(pImgResize);
    cvNamedWindow("第五个字符");
    cvShowImage("第五个字符", pImgCharFive);

    ROI_rect1.x = 0.5*(letter[11] + letter[10]) - 10;
    ROI_rect1.y = 0;
    ROI_rect1.width = 20;
    ROI_rect1.height = 40;
    cvSetImageROI(pImgResize, ROI_rect1);
    cvCopy(pImgResize, pImgCharSix, NULL); //获取第6个字符
    cvResetImageROI(pImgResize);
    cvNamedWindow("第六个字符");
    cvShowImage("第六个字符", pImgCharSix);

    ROI_rect1.x = 0.5*(letter[13] + letter[12]) - 10;
    ROI_rect1.y = 0;
    ROI_rect1.width = 20;
    ROI_rect1.height = 40;
    cvSetImageROI(pImgResize, ROI_rect1);
    cvCopy(pImgResize, pImgCharSeven, NULL); //获取第7个字符
    cvResetImageROI(pImgResize);
    cvNamedWindow("第七个字符");
    cvShowImage("第七个字符", pImgCharSeven);


    //字符识别
    CodeRecognize(pImgCharOne, 3, 0);
    CodeRecognize(pImgCharTwo, 1, 1);
    CodeRecognize(pImgCharThree, 2, 2);
    CodeRecognize(pImgCharFour, 2, 3);
    CodeRecognize(pImgCharFive, 0, 4);
    CodeRecognize(pImgCharSix, 0, 5);
    CodeRecognize(pImgCharSeven, 0, 6);

    //输出字符在控制台上
    string outFile = "";
    int carnum;
    for (carnum = 0; carnum < 7; carnum++)
    {
        outFile += G_PlateChar[carnum];
    }

    cout << "车牌为:";
    cout << outFile << endl;

    cvWaitKey();

    // 释放内存
    delete[]num_h;
    num_h = NULL;

}
//* -----------------------字符识别-----------------------------------------//
// --Input: 
//               IplImage *pImgCharOne              // 字符图片
//               IplImage *pImgCharTwo
//               IplImage *pImgCharThree
//               IplImage *pImgCharFour
//               IplImage *pImgCharFive
//               IplImage *pImgCharSix
//               IplImage *pImgCharSeven
//               int num                    // 数字字符汉字识别用,
//                                          // 0 数字 1英文 2英文和数字 3中文
//               int char_num               // 第char_num个车牌字符
//-- Output:   
//               char *G_G_PlateChar[7]      // 车牌号 
//--  Description:  
//               利用垂直投影法和车牌的特征结合分割字符区域
//-------------------------------------------------------------------------*/  

//说明:垂直投影 
//对于二值图像,水平方向的投影就是每行的非零像素值的个数,在这里就是1或者255,
//垂直投影就是每列图像数据中非零像素值的个数。
//车牌特征  就是 车牌一般大小  即长宽比例,还有每个字符之间的间隔 第二个和第三个字符之间间
//隔大些,车牌里有汉字、数字和字母,第一个是汉字,第二个是字母,后面是字母和数字,大体也就这么些特征
int OpencvCar::CodeRecognize(IplImage *imgTest, int num, int char_num)
{
    if (imgTest == NULL) { return 0; }

    int i = 0, j = 0, k = 0, t = 0;//循环变量
    int  char_start = 0, char_end = 0;//*PlateCode[TEMPLETENUM] 车牌字符里字母、数字、汉字起始位置
    int num_t[CHARACTER] = { 0 };
    switch (num)//这里这样分 可以提高效率,并且提高了识别率
    {
    case 0:  char_start = 0;         // 数字
        char_end = 9;
        break;
    case 1:  char_start = 10;        // 英文
        char_end = 35;
        break;
    case 2:  char_start = 0;       // 英文和数字
        char_end = 35;
        break;
    case 3:  char_start = 36;       // 中文
        char_end = TEMPLETENUM - 1;
        break;
    default: break;
    }
    // 提取前8个特征  前8个特征 可以说是固定位置的值  固定算法
    for (k = 0; k < 8; k++)
    {
        for (j = int(k / 2) * 10; j= 5)  //特征值 大于5 
            {
                if (abs(num_t[i] - Num_Templete[k][i]) <= 1)
                    matchnum += 2;
            }
            else if (num_t[i] == Num_Templete[k][i])
            {
                matchnum += 2;
            }
        }
        if (matchnum > matchnum_max)
        {
            matchnum_max = matchnum;  //保留最大的 匹配 
            matchcode = k;  //记录 识别的字符的 索引 
            //matchtempnum[j]=matchnum_min
        }
    }
    //识别输出  存放输出结果
    G_PlateChar[char_num] = PlateCode[matchcode]; //保存下该字符
    cout << "    匹配成功,匹配字符为: " << G_PlateChar[char_num] << endl;
}

3.CarReco.cpp 车牌识别主程序

程序

#include "OpencvCar.h"
using namespace std;
int main()
{
    IplImage *src;   //原始图片                        
    IplImage *pImgCanny; //二值化的图   
    IplImage *pImgResize;  //归一化的车牌区域灰度图                  
    IplImage *pImgCharOne;  //字符图片             
    IplImage *pImgCharTwo;
    IplImage *pImgCharThree;
    IplImage *pImgCharFour;
    IplImage *pImgCharFive;
    IplImage *pImgCharSix;
    IplImage *pImgCharSeven;
    //实例化OpencvCar类 
    OpencvCar cvCar;
    cout << "欢迎使用车牌识别系统" << endl;
    src = NULL;
    const char *filepath;
    filepath = "car3.jpg";  //等待识别的图片位置
    src = cvLoadImage(filepath); //读取图片
    cvNamedWindow("原图像");
    cvShowImage("原图像", src);
    pImgCanny = cvCreateImage(cvSize(src->width, src->height), IPL_DEPTH_8U, 1);     //  2值化后图片大小初始化
    cvCvtColor(src, pImgCanny, CV_RGB2GRAY);  //转化为灰度图  openCV函数 Y = 0.299*R + 0.587*G + 0.114*B   //  转为灰度图      Y=0.299*R + 0.587*G + 0.114*B
    cvSmooth(pImgCanny, pImgCanny, CV_GAUSSIAN, 3, 0, 0); 
    //二值化
    cvCar.Threshold(pImgCanny, pImgCanny);
    cvNamedWindow("二值化图像");
    cvShowImage("二值化图像", pImgCanny);
    //车牌定位
    cvCar.PlateAreaSearch(pImgCanny, src);
    //字符分割并识别
    cvCar.SegmentPlate();
    return 0;
}

#运行效果

效果