《深入理解OpenCV》——ALPR

1 概述

车牌识别系统的主要组成部分包括: 车牌图像采集、 车牌图像预处理、 车牌定位、 车牌字符分割和车牌字符识别。

Created with Raphaël 2.1.0 车牌图像采集 车牌预处理 车牌定位 车牌字符分割 车牌字符识别 结束

1.1 图像采集

通过摄像头或摄像机拍摄含有车辆牌照的图像,采用拍摄的含有汽车车牌的汽车图片作为原始识别图像。

1.2 车牌预处理

对采集的车牌图像输入计算机进行预处理, 突出车牌的主要特征, 便于提取车牌信息。图像预处理主要是对待处理图像进行图像格式的转换和压缩、 图像去噪、 图像增强等操作,尽量剔除那些无用的信息。

1.3 车牌定位:

对车牌图像中的车牌字符区域进行定位, 该过程主要利用车牌区域的特征信息。

  • 基于边缘检测和形态学变换的车牌定位算法

    • 通常所说的边缘指的是周围像素灰度有阶跃变化的像素点的集合。该算法利用了车牌区域字符垂直边缘较丰富,而车身非车牌区域主要是水平边缘而垂直边缘较少的特征。通过边缘检测算子可以得到图像的垂直边缘图,然后将垂直边缘图通过形态学变换(如膨胀和腐蚀等)将垂直边缘较密集的区域融合成一个整体,这样可以得到包括车牌在内的多个区域。最后通过车牌区域的一些先验知识(如长宽比、颜色等)筛选出车牌区域。这种方法对光照不敏感,算法简单,速度快,但虚警率较高,适合图像中只包含车身或者背景的图像纹理简单的场景。该算法的其难点在于伪车牌区域如车灯、散热器、车身字符等的去除。
  • 基于颜色特征的车牌定位算法
    该算法利用了车牌区域车牌背景和字符具有相对固定的颜色搭配的特性。

    • 首先将RGB色彩模式转化为HSV或HSL色彩模式,然后根据色调、饱和度、亮度的范围判断像素的颜色,并将与车牌底色相近的区域提取出来,最后进行形态处理并根据车牌的先验知识筛选出车牌区域。该算法在车牌颜色已知和彩色图片质量较好的情况下可以较准确的定位出车牌区域,但是如果车牌区域和车身颜色相近时,该算法很难的定位出车牌区域。另外,车牌磨损、光照变化等不确定因素会严重影响该算法的效果。边界变形非常敏感,如果车牌区域边界是模糊或者扭曲的,或者散热器罩附近有很多横的或竖的边缘,该方法很难定位出车牌。在处理车牌底色为蓝色、 车牌信息为白色的汽车车牌图像时, 采用 HSV模型[识别蓝色, 用 RGB模型识别白色, 分别扫描 Y方向蓝色像素点和 X方向白色像素点, 找出车牌区域, 并以一定的长宽比进行裁剪, 使用HSV彩图提取图像。

常用的车牌定位算法还有: 基于数学形态学车牌定位、 基于投影法的车牌定位、 基于字符纹理分析车牌定位、基于机器学习的车牌定位算法、基于小波变换的车牌定位算法、基于Hough变换和轮廓线法的车牌定位算法等。

1.4 字符分割:

为得到车牌信息中的每个字符, 需要对定位后的车牌区域中的字符进行分割。通常在分割之前需对车牌进行倾斜校正。目前常用的倾斜校正算法有Hough变换,Radon变换,PCA方法,旋转投影的方法。

  • 在倾斜校正这一步,只介绍下PCA方法,因为其它方法尤其是Hough和Radon可供参考的资料很多。PCA方法主体思想是运用主元分析的方法求取坐标变换矩阵来对车牌图像进行倾斜校正。先将原始的像素坐标矩阵中心化后求取二维协方差矩阵,再求取该协方差矩阵的特征值和特征向量,得到车牌区域的倾斜方向,最后通过旋转矩阵校正车牌图像。垂直边缘特征主元分析算法流程图如下
Created with Raphaël 2.1.0 车牌灰度图片 Sobel算子检测垂直边缘 建立垂直边缘特征矩阵 特征矩阵中心化(样本散布矩阵) 求矩阵较大特值对应特征向量的方向(车牌倾斜方向) 坐标变换实现车牌倾斜校正

我国的车牌有蓝底白字、黄底黑字、黑底白字、白底黑字等颜色搭配,车牌区域一般由7个字符组成。为了识别出确切的车牌信息,目前最好的解决方案是将车牌区域字符单独提取出来分开识别。目前常用的车牌字符分割方法有投影法、模板匹配法、聚类连通域分析法。

  • 投影法。投影法是目前字符分割最常用的方法,该方法简单直观,利用了字符之间存在的固有空隙。如果将车牌区域进行垂直方向投影,就会存在明显的波峰波谷,通过设定一定阈值区分出字符和空隙,就可以将字符分割出来。

  • 聚类连通域分析法。聚类连通域分析法的基本思想是将相互连通的区域看成一个整体,车牌区域通常由7个字符组成,汉字有可能存在多个连通区域,但是字母和数字正常情况下都是连通的。这样可以通过在二值图像上搜寻连通区域,并得到连通区域的外接矩形,正常情况下汉字区域外可以得到6个连通区域即可分割出6个字符,汉字区域可以结合车牌区域的先验知识确定出,从而完成车牌区域的字符分割。该方法具有很髙的鲁棒性,但是算法设计较复杂,需要综合考虑各种可能出现的情况。

1.5 字符识别

对得到的单个车牌字符进行识别。分类的方法很多一般在车牌识别中通常采用BP神经网络。

2 车牌检测

在《深入理解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;
    }
}

《深入理解OpenCV》——ALPR_第1张图片

《深入理解OpenCV》——ALPR_第2张图片

3 车牌号识别

一般就是采用特征检测+分类的方法。以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");

4 OpenALPR

OpenALPR的官方网址:http://www.openalpr.com/
OpenALPR的github地址:https://github.com/openalpr/openalpr

5 EasyPR

easyPR作者的博客:http://www.cnblogs.com/subconscious/
easyPR的github地址:https://github.com/liuruoze/EasyPR

6 总结

Mastering OpenCV with Practical Computer Vision Projects这本书中以西班牙的车牌为例,对该源码提供的几张车牌确实效果不错,然而在其他数据集上,只能用不理想来形容,适应的场景有限。但它的处理逻辑清晰明了,把车牌识别划分为了两个过程:即车牌检测和字符识别两个过程。再针对这两个过程分别进行处理,极具参考价值。因为没有实际项目的需求,仅仅也只是自己练练手,所以对两个开源的ALPR软件(国外的OpenALRP以及国内的EasyPR)没有过多的研究。如果有机会再补全该片文章(车牌作为车辆的唯一凭证,应用极具广泛,虽然车牌识别已发展多年,但依旧值得开发,丰富第四屏的应用)。

参考资料:
毛江平,车载云台摄像机的车牌识别系统研究
黄山,车牌识别技术的研究和实现
杨思源,基于OPENCV的车辆牌照识别系统研究
Sina Moayed Baharlou, Fast and Adaptive License Plate Recognition
。。。

你可能感兴趣的:(图像处理,opencv,计算机视觉,车牌,车牌识别)